mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ab3fd9e3 | ||
|
|
687804d0b4 | ||
|
|
804de2c13c | ||
|
|
6133451d23 | ||
|
|
8a295f97ce | ||
|
|
d4842daf07 | ||
|
|
0aaca1bb7d | ||
|
|
d5c376b4dd | ||
|
|
8faeb606d7 | ||
|
|
be6b8afedc | ||
|
|
4baa026a3e | ||
|
|
515c4ee205 | ||
|
|
d884b42472 | ||
|
|
f3abeb528b | ||
|
|
78e552853d | ||
|
|
8dc1a664f1 | ||
|
|
797cb61a3f | ||
|
|
d223a8ce23 | ||
|
|
3da10149ee | ||
|
|
c8e9e576fc | ||
|
|
f5ba8312a7 | ||
|
|
f95a1ccfd1 | ||
|
|
af52a48289 | ||
|
|
bce53a9fe3 | ||
|
|
937d5f3f1c | ||
|
|
31c90b0d19 | ||
|
|
78664ec5f6 | ||
|
|
d2d229125b | ||
|
|
7de518432b | ||
|
|
079ae5cd10 | ||
|
|
060780eb7e | ||
|
|
6391dcdf72 | ||
|
|
d172d7da62 | ||
|
|
d7575f30c3 |
+42
-5
@@ -7,17 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.6] - 2026-02-03
|
||||
|
||||
### Added / Changed
|
||||
|
||||
- **Enhanced Change Management Module**:
|
||||
- New `semantica.change_management` module with persistent version storage and audit trails
|
||||
- **W3C PROV-O Compliant Provenance Tracking** (#254, #246):
|
||||
- Comprehensive provenance tracking system with W3C PROV-O compliance across all 17 Semantica modules
|
||||
- **Core Module**: `ProvenanceManager`, W3C PROV-O schemas, storage backends (InMemory, SQLite), SHA-256 integrity verification
|
||||
- **Module Integrations**: Semantic Extract, LLMs (Groq, OpenAI, HuggingFace, LiteLLM), Pipeline, Context, Ingest, Embeddings, Graph/Vector/Triplet stores, Reasoning, Conflicts, Deduplication, Export, Parse, Normalize, Ontology, Visualization
|
||||
- **Features**: Complete lineage tracking (Document → Chunk → Entity → Relationship → Graph), LLM tracking (tokens, costs, latency), source tracking, bridge axioms for domain transformations
|
||||
- **Compliance Infrastructure**: W3C PROV-O, FDA 21 CFR Part 11, SOX, HIPAA, TNFD
|
||||
- **Testing**: 237 tests covering core functionality, all 17 module integrations, edge cases, backward compatibility
|
||||
- **Design**: Opt-in with `provenance=False` by default, zero breaking changes, no new dependencies
|
||||
- Contributed by @KaifAhmad1
|
||||
|
||||
- **Enhanced Change Management Module** (#248, #243):
|
||||
- Enterprise-grade version control for knowledge graphs and ontologies with persistent storage and audit trails
|
||||
- **Core Classes**: `TemporalVersionManager` (KG versioning), `OntologyVersionManager` (ontology versioning), `ChangeLogEntry` (metadata)
|
||||
- **Storage**: SQLite (persistent) and in-memory backends with thread-safe operations
|
||||
- **Features**: SHA-256 checksums, detailed entity/relationship diffs, structural ontology comparison, email validation
|
||||
- **Compliance**: HIPAA, SOX, FDA 21 CFR Part 11 support with immutable audit trails
|
||||
- **Compliance Infrastructure**: HIPAA, SOX, FDA 21 CFR Part 11 with immutable audit trails
|
||||
- **Testing**: 104 tests (100% pass) - unit, integration, compliance, performance, edge cases
|
||||
- **Performance**: 17.6ms for 10k entities, 510+ ops/sec concurrent, handles 5k+ entity graphs
|
||||
- **Migration**: Backward compatible, simplified class names, zero external dependencies
|
||||
- Contributed by @KaifAhmad1
|
||||
|
||||
- CSV Ingestion Enhancements (PR #244 by @saloni0318)
|
||||
- Auto-detect CSV encoding (chardet) and delimiter (csv.Sniffer)
|
||||
@@ -31,8 +44,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Tests: Register integration mark and tidy ingest test warnings (PR #241 by @KaifAhmad1)
|
||||
- Introduced integration test marker and reduced noisy warnings in ingest tests
|
||||
|
||||
- Tests (ingest): Add unit tests for file, web, and feed ingestors (PR #239 by @Mohammed2372)
|
||||
- Broadened ingest test coverage across multiple source types
|
||||
- **Ingest Unit Tests** (#239, #232):
|
||||
- Comprehensive unit tests for ingestion modules (file, web, and feed ingestors)
|
||||
- **Coverage**: File scanning (local/cloud S3/GCS/Azure), web ingestion (URL/sitemap/robots.txt), RSS/Atom feed parsing
|
||||
- **Testing**: 998 lines of test code with mocked external dependencies for fast, isolated execution
|
||||
- **Results**: file_ingestor (86%), web_ingestor (86%), feed_ingestor (80%) coverage
|
||||
- Covers happy paths, edge cases, and error handling
|
||||
- Contributed by @Mohammed2372
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Temperature Compatibility Fix** (#256, #252):
|
||||
- Fixed hardcoded `temperature=0.3` that broke compatibility with models requiring specific temperature values (e.g., gpt-5-mini)
|
||||
- Added `_add_if_set` helper method to `BaseProvider` that only passes parameters when explicitly set
|
||||
- When `temperature=None`, parameter is omitted allowing APIs to use model defaults
|
||||
- Updated all 5 providers: OpenAI, Groq, Gemini, Ollama, DeepSeek
|
||||
- Reduced code by ~85 lines with cleaner parameter handling
|
||||
- Comprehensive test coverage added (10 temperature tests, all passing)
|
||||
- Backward compatible - no breaking changes
|
||||
- Contributed by @F0rt1s and @IGES-Institut
|
||||
|
||||
- **JenaStore Empty Graph Bug** (#257, #258):
|
||||
- Fixed `ProcessingError: Graph not initialized` when operating on empty (but initialized) graphs
|
||||
- Replaced implicit `if not self.graph:` checks with explicit `if self.graph is None:` validation in 5 methods (`add_triplets`, `get_triplets`, `delete_triplet`, `execute_sparql`, `serialize`)
|
||||
- Properly distinguishes `None` (uninitialized) from empty graphs (initialized with 0 triplets)
|
||||
- Unblocks benchmarking suite, fresh deployments, and testing workflows
|
||||
- Contributed by @ZohaibHassan16
|
||||
|
||||
## [0.2.5] - 2026-01-27
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="semantica_logo.png" alt="Semantica Logo" width="460"/>
|
||||
<img src="Semantica Updated Logo.png" alt="Semantica Logo" width="460"/>
|
||||
|
||||
# 🧠 Semantica
|
||||
### Open-Source Semantic Layer & Knowledge Engineering Framework
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
### ⭐ Give us a Star • 🍴 Fork us • 💬 Join our Discord
|
||||
|
||||
> **Transform Choas into Intelligence. Build AI systems that are explainable, traceable, and trustworthy — not black boxes.**
|
||||
> **Transform Chaos into Intelligence. Build AI systems that are explainable, traceable, and trustworthy — not black boxes.**
|
||||
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
## 🚀 Why Semantica?
|
||||
|
||||
**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.
|
||||
**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 trustworthy.
|
||||
|
||||
Perfect for **high-stakes domains** where mistakes have real consequences.
|
||||
|
||||
@@ -56,8 +56,8 @@ print(f"Built KG with {len(kg.get('entities', []))} entities")
|
||||
| **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 |
|
||||
| Rule-based governance | Entity relationships & ontologies | W3C PROV-O compliant lineage |
|
||||
| Production-grade QA | Multi-hop graph reasoning | Source tracking & integrity verification |
|
||||
|
||||
---
|
||||
|
||||
@@ -69,19 +69,19 @@ print(f"Built KG with {len(kg.get('entities', []))} entities")
|
||||
|
||||
| Feature | Benefit |
|
||||
|:--------|:--------|
|
||||
| **Auditable** | Complete provenance tracking with full audit trails |
|
||||
| **Auditable** | Complete provenance tracking with W3C PROV-O compliance |
|
||||
| **Explainable** | Transparent reasoning paths with entity relationships |
|
||||
| **Provenance-Aware** | Source-level provenance from documents to responses |
|
||||
| **Provenance-Aware** | End-to-end lineage from documents to responses |
|
||||
| **Validated** | Built-in conflict detection, deduplication, QA |
|
||||
| **Governed** | Rule-based validation and semantic consistency |
|
||||
| **Version Control** | Enterprise-grade change management with HIPAA/SOX/FDA compliance |
|
||||
| **Version Control** | Enterprise-grade change management with integrity verification |
|
||||
|
||||
### Perfect For High-Stakes Use Cases
|
||||
|
||||
| 🏥 **Healthcare** | 💰 **Finance** | ⚖️ **Legal** |
|
||||
|:-----------------:|:--------------:|:------------:|
|
||||
| Clinical decisions | Fraud detection | Evidence-backed research |
|
||||
| Drug interactions | Regulatory compliance | Contract analysis |
|
||||
| Drug interactions | Regulatory support | Contract analysis |
|
||||
| Patient safety | Risk assessment | Case law reasoning |
|
||||
|
||||
| 🔒 **Cybersecurity** | 🏛️ **Government** | 🏭 **Infrastructure** | 🚗 **Autonomous** |
|
||||
@@ -94,7 +94,7 @@ print(f"Built KG with {len(kg.get('entities', []))} entities")
|
||||
- **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
|
||||
- **Enterprise AI** — Governed, auditable platforms that support compliance
|
||||
|
||||
### Integrations
|
||||
|
||||
@@ -158,11 +158,11 @@ The **semantic gap** is the fundamental disconnect between what AI systems can p
|
||||
| Feature | Traditional RAG | Semantica |
|
||||
|:--------|:----------------|:----------|
|
||||
| **Reasoning** | ❌ Black-box answers | ✅ Explainable reasoning paths |
|
||||
| **Provenance** | ❌ No provenance | ✅ Source-level provenance |
|
||||
| **Provenance** | ❌ No provenance | ✅ W3C PROV-O compliant lineage tracking |
|
||||
| **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 |
|
||||
| **Compliance** | ❌ No audit trails | ✅ Complete audit trails with integrity verification |
|
||||
|
||||
---
|
||||
|
||||
@@ -181,9 +181,9 @@ The **semantic gap** is the fundamental disconnect between what AI systems can p
|
||||
- 📐 **Ontology Induction** — Automated domain rule generation
|
||||
- 🔄 **Deduplication** — Jaro-Winkler similarity, conflict resolution
|
||||
- ✅ **Quality Assurance** — Conflict detection, validation
|
||||
- 📊 **Provenance Tracking** — Source, time, confidence metadata
|
||||
- 📊 **Provenance Tracking** — W3C PROV-O compliant lineage tracking across all modules
|
||||
- 🧠 **Reasoning Traces** — Explainable inference paths
|
||||
- 🔐 **Change Management** — Version control with audit trails, checksums, HIPAA/SOX/FDA compliance
|
||||
- 🔐 **Change Management** — Version control with audit trails, checksums, compliance support
|
||||
|
||||
### 3️⃣ Output Layer — Auditable Knowledge Assets
|
||||
- 📊 **Knowledge Graphs** — Queryable, temporal, explainable
|
||||
@@ -202,9 +202,9 @@ The **semantic gap** is the fundamental disconnect between what AI systems can p
|
||||
|
||||
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
|
||||
- **🏥 Healthcare & Life Sciences** — Clinical decision support, drug interaction analysis, medical literature reasoning, patient safety tracking
|
||||
- **💰 Finance & Risk** — Fraud detection, regulatory support (SOX, GDPR, MiFID II), credit risk assessment, algorithmic trading validation
|
||||
- **⚖️ Legal & Compliance** — Evidence-backed legal research, contract analysis, regulatory change tracking, 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
|
||||
@@ -450,7 +450,7 @@ print(f"Classes: {len(custom_ontology.classes)}")
|
||||
|
||||
### Change Management & Version Control
|
||||
|
||||
> **Enterprise-Grade Versioning** • Persistent Storage • Audit Trails • HIPAA/SOX/FDA Compliance • SHA-256 Checksums
|
||||
> **Version Control for Knowledge Graphs & Ontologies** • **SQLite & In-Memory Storage** • **SHA-256 Integrity Verification**
|
||||
|
||||
```python
|
||||
from semantica.change_management import TemporalVersionManager, OntologyVersionManager
|
||||
@@ -475,16 +475,93 @@ print(f"Entities modified: {diff['summary']['entities_modified']}")
|
||||
is_valid = kg_manager.verify_checksum(snapshot)
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- 🔐 **Persistent Storage** — SQLite and in-memory backends
|
||||
**What We Provide:**
|
||||
- 🔐 **Persistent Storage** — SQLite and in-memory backends implemented
|
||||
- 📊 **Detailed Diffs** — Entity-level and relationship-level change tracking
|
||||
- ✅ **Data Integrity** — SHA-256 checksums with tamper detection
|
||||
- 🏥 **Compliance Ready** — HIPAA, SOX, FDA 21 CFR Part 11 support
|
||||
- ⚡ **High Performance** — 17.6ms for 10k entities, 510+ ops/sec concurrent
|
||||
- 🧪 **Fully Tested** — 104 tests covering real-world scenarios
|
||||
- 📝 **Standardized Metadata** — ChangeLogEntry with author, timestamp, description
|
||||
- ⚡ **Performance Tested** — Benchmarked with 10k entities
|
||||
- 🧪 **Test Coverage** — 104 tests covering core functionality
|
||||
|
||||
**Compliance Note:** Provides technical infrastructure (audit trails, checksums, temporal tracking) that supports compliance efforts for HIPAA, SOX, FDA 21 CFR Part 11. Organizations must implement additional policies and procedures for full regulatory compliance.
|
||||
|
||||
[**Documentation: Change Management**](docs/reference/change_management.md) • [**Usage Guide**](semantica/change_management/change_management_usage.md)
|
||||
|
||||
### Provenance Tracking — W3C PROV-O Compliant Lineage
|
||||
|
||||
> **W3C PROV-O Implementation** • **17 Module Integrations** • **Opt-In Design** • **Zero Breaking Changes**
|
||||
|
||||
**⚠️ Compliance Note:** Provides technical infrastructure for provenance tracking that supports compliance efforts. Organizations must implement additional policies, procedures, and controls for full regulatory compliance.
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
|
||||
# Enable provenance tracking - just add provenance=True
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
entities = ner.extract(
|
||||
text="Apple Inc. was founded by Steve Jobs.",
|
||||
source="biography.pdf"
|
||||
)
|
||||
|
||||
# Track LLM calls with costs and latency
|
||||
llm = GroqLLMWithProvenance(provenance=True, model="llama-3.1-70b")
|
||||
response = llm.generate("Summarize the document")
|
||||
|
||||
# Store in graph with complete lineage
|
||||
graph = GraphStoreWithProvenance(provenance=True)
|
||||
graph.add_node(entity, source="biography.pdf")
|
||||
|
||||
# Retrieve complete provenance
|
||||
lineage = ner._prov_manager.get_lineage("entity_id")
|
||||
print(f"Source: {lineage['source']}")
|
||||
print(f"Lineage chain: {lineage['lineage_chain']}")
|
||||
```
|
||||
|
||||
**What We Provide:**
|
||||
- ✅ **W3C PROV-O Implementation** — Data schemas implementing prov:Entity, prov:Activity, prov:Agent, prov:wasDerivedFrom
|
||||
- ✅ **17 Module Integrations** — Provenance-enabled versions of semantic extract, LLMs, pipeline, context, ingest, embeddings, reasoning, conflicts, deduplication, export, parse, normalize, ontology, visualization, graph/vector/triplet stores
|
||||
- ✅ **Opt-In Design** — Zero breaking changes, `provenance=False` by default
|
||||
- ✅ **Lineage Tracking** — Document → Chunk → Entity → Relationship → Graph lineage chains
|
||||
- ✅ **LLM Tracking** — Token counts, costs, and latency tracking for LLM calls
|
||||
- ✅ **Source Tracking Fields** — Document identifiers, page numbers, sections, and quote fields in schemas
|
||||
- ✅ **Storage Backends** — InMemoryStorage (fast) and SQLiteStorage (persistent) implemented
|
||||
- ✅ **Bridge Axioms** — BridgeAxiom and TranslationChain classes for domain transformations (L1 → L2 → L3)
|
||||
- ✅ **Integrity Verification** — SHA-256 checksum computation and verification functions
|
||||
- ✅ **No New Dependencies** — Uses Python stdlib only (sqlite3, json, dataclasses)
|
||||
|
||||
**Supported Modules:**
|
||||
```python
|
||||
# Semantic Extract
|
||||
from semantica.semantic_extract.semantic_extract_provenance import (
|
||||
NERExtractorWithProvenance, RelationExtractorWithProvenance, EventDetectorWithProvenance
|
||||
)
|
||||
|
||||
# LLM Providers
|
||||
from semantica.llms.llms_provenance import (
|
||||
GroqLLMWithProvenance, OpenAILLMWithProvenance, HuggingFaceLLMWithProvenance
|
||||
)
|
||||
|
||||
# Storage & Processing
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
from semantica.vector_store.vector_store_provenance import VectorStoreWithProvenance
|
||||
from semantica.pipeline.pipeline_provenance import PipelineWithProvenance
|
||||
|
||||
# ... and 12 more modules
|
||||
```
|
||||
|
||||
**High-Stakes Use Cases:**
|
||||
- 🏥 **Healthcare** — Clinical decision audit trails with source tracking
|
||||
- 💰 **Finance** — Fraud detection provenance with complete lineage
|
||||
- ⚖️ **Legal** — Evidence chain of custody with temporal tracking
|
||||
- 🔒 **Cybersecurity** — Threat attribution with relationship tracking
|
||||
- 🏛️ **Government** — Policy decision audit trails with integrity verification
|
||||
|
||||
**Note:** Provenance tracking provides the *technical infrastructure* for compliance. Organizations must implement additional policies and procedures to meet specific regulatory requirements (HIPAA, SOX, FDA 21 CFR Part 11, etc.).
|
||||
|
||||
[**Documentation: Provenance Tracking**](semantica/provenance/provenance_usage.md)
|
||||
|
||||
### Context Engineering & Memory Systems
|
||||
|
||||
> **Persistent Memory** • **Context Graph** • **Context Retriever** • **Hybrid Retrieval (Vector + Graph)** • **Production Graph Store (Neo4j)** • **Entity Linking** • **Multi-Hop Reasoning**
|
||||
@@ -866,20 +943,6 @@ print(f"Found {len(results)} results")
|
||||
|
||||
[**See Advanced Examples**](https://github.com/Hawksight-AI/semantica/tree/main/cookbook/advanced) — Advanced extraction, graph analytics, reasoning, and more.
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
### Q1 2026
|
||||
- [x] Core framework (v1.0)
|
||||
- [x] GraphRAG engine
|
||||
- [x] 6-stage ontology pipeline
|
||||
- [x] Advanced reasoning v2 (Rete, Forward/Backward Chaining)
|
||||
- [ ] Quality assurance features and Quality Assurance module
|
||||
- [ ] Enhanced multi-language support
|
||||
- [ ] Evals
|
||||
- [ ] Real-time streaming improvements
|
||||
|
||||
### Q2 2026
|
||||
- [ ] Multi-modal processing
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
+1
-1
@@ -46,7 +46,7 @@ semantica/
|
||||
│ │ └── custom.css # Custom styling
|
||||
│ └── assets/
|
||||
│ └── img/
|
||||
│ └── semantica_logo.png
|
||||
│ └── Semantica Updated Logo.png
|
||||
└── site/ # Generated site (created by mkdocs build)
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
+5
-5
@@ -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.5},
|
||||
version = {0.2.6},
|
||||
doi = {10.5281/zenodo.XXXXXXX}
|
||||
}
|
||||
```
|
||||
|
||||
### APA
|
||||
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
|
||||
Hawksight AI. (2026). *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering* (Version 0.2.6) [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.5, GitHub, 2026, https://github.com/Hawksight-AI/semantica.
|
||||
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.6, 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.5. GitHub, 2026. https://github.com/Hawksight-AI/semantica.
|
||||
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.6. 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.5, 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.6, GitHub, 2026. [Online]. Available: https://github.com/Hawksight-AI/semantica
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="assets/img/semantica_logo.png" alt="Semantica Logo" width="450" height="auto">
|
||||
<img src="assets/img/Semantica Updated Logo.png" alt="Semantica Logo" width="450" height="auto">
|
||||
|
||||
<h1>🧠 Semantica</h1>
|
||||
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
# Provenance Tracking Module
|
||||
|
||||
**W3C PROV-O compliant provenance tracking for high-stakes domains requiring complete traceability**
|
||||
|
||||
## Overview
|
||||
|
||||
The Semantica provenance module provides W3C PROV-O compliant tracking for knowledge graphs, enabling complete end-to-end lineage from source documents to query responses. Designed for high-stakes domains where every decision must be explainable and auditable.
|
||||
|
||||
### Key Features
|
||||
|
||||
- ✅ **W3C PROV-O Compliant** — Implements PROV-O ontology (prov:Entity, prov:Activity, prov:Agent, prov:wasDerivedFrom)
|
||||
- ✅ **All 17 Modules Integrated** — Complete coverage across Semantica
|
||||
- ✅ **Source Tracking** — Document identifiers, page numbers, sections, and direct quotes supported
|
||||
- ✅ **Zero Breaking Changes** — 100% backward compatible, opt-in only
|
||||
- ✅ **Multiple Storage Backends** — InMemory (fast) and SQLite (persistent)
|
||||
- ✅ **Bridge Axiom Support** — Translation chain tracking for domain transformations (L1 → L2 → L3)
|
||||
- ✅ **Integrity Verification** — SHA-256 checksums for tamper detection
|
||||
- ✅ **Complete Lineage Tracing** — End-to-end from document to response
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
The provenance module is included with Semantica. No additional installation required.
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceManager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### ProvenanceManager
|
||||
|
||||
Central manager for all provenance tracking operations.
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
# Initialize with in-memory storage (default)
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Initialize with persistent SQLite storage
|
||||
manager = ProvenanceManager(storage_path="provenance.db")
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `track_entity(entity_id, source, entity_type, **metadata)` — Track entity provenance
|
||||
- `track_relationship(relationship_id, source, subject, predicate, obj, **metadata)` — Track relationship provenance
|
||||
- `track_chunk(chunk_id, source_document, chunk_text, start_char, end_char, **metadata)` — Track document chunk provenance
|
||||
- `track_property_source(entity_id, property_name, value, source, **metadata)` — Track property-level provenance
|
||||
- `get_lineage(entity_id)` — Retrieve complete lineage for an entity
|
||||
- `get_statistics()` — Get provenance statistics
|
||||
- `get_all_entries()` — Retrieve all provenance entries
|
||||
|
||||
### Storage Backends
|
||||
|
||||
#### InMemoryStorage
|
||||
|
||||
Fast, non-persistent storage for development and testing.
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceManager, InMemoryStorage
|
||||
|
||||
manager = ProvenanceManager(storage=InMemoryStorage())
|
||||
```
|
||||
|
||||
#### SQLiteStorage
|
||||
|
||||
Persistent storage for production use.
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceManager, SQLiteStorage
|
||||
|
||||
storage = SQLiteStorage("provenance.db")
|
||||
manager = ProvenanceManager(storage=storage)
|
||||
```
|
||||
|
||||
### Data Schemas
|
||||
|
||||
#### ProvenanceEntry
|
||||
|
||||
Core data structure for provenance tracking.
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceEntry
|
||||
from datetime import datetime
|
||||
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
source="document.pdf",
|
||||
timestamp=datetime.now(),
|
||||
entity_type="named_entity",
|
||||
metadata={"text": "Apple Inc.", "confidence": 0.95}
|
||||
)
|
||||
```
|
||||
|
||||
#### SourceReference
|
||||
|
||||
Structured source information with page and section details.
|
||||
|
||||
```python
|
||||
from semantica.provenance import SourceReference
|
||||
|
||||
source = SourceReference(
|
||||
document="research_paper.pdf",
|
||||
page=5,
|
||||
section="Results",
|
||||
confidence=0.98
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Integrations
|
||||
|
||||
All Semantica modules have provenance-enabled versions. Enable tracking by setting `provenance=True`.
|
||||
|
||||
### Semantic Extract
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract.semantic_extract_provenance import (
|
||||
NERExtractorWithProvenance,
|
||||
RelationExtractorWithProvenance,
|
||||
EventDetectorWithProvenance,
|
||||
CoreferenceResolverWithProvenance,
|
||||
TripletExtractorWithProvenance
|
||||
)
|
||||
|
||||
# Named Entity Recognition with provenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
entities = ner.extract(
|
||||
text="Apple Inc. was founded by Steve Jobs in Cupertino.",
|
||||
source="company_history.pdf"
|
||||
)
|
||||
|
||||
# Access provenance manager
|
||||
prov_manager = ner._prov_manager
|
||||
lineage = prov_manager.get_lineage("entity_id")
|
||||
```
|
||||
|
||||
**Tracks:** Entity text, labels, confidence scores, source documents, character positions, extraction timestamps
|
||||
|
||||
### LLM Providers
|
||||
|
||||
```python
|
||||
from semantica.llms.llms_provenance import (
|
||||
GroqLLMWithProvenance,
|
||||
OpenAILLMWithProvenance,
|
||||
HuggingFaceLLMWithProvenance,
|
||||
LiteLLMWithProvenance
|
||||
)
|
||||
|
||||
# Groq LLM with provenance
|
||||
llm = GroqLLMWithProvenance(
|
||||
provenance=True,
|
||||
model="llama-3.1-70b"
|
||||
)
|
||||
|
||||
response = llm.generate("What is artificial intelligence?")
|
||||
|
||||
# Access cost and performance data
|
||||
stats = llm._prov_manager.get_statistics()
|
||||
```
|
||||
|
||||
**Tracks:** Model name, prompt/completion tokens, API costs, latency, generation parameters, prompts and responses
|
||||
|
||||
### Pipeline Execution
|
||||
|
||||
```python
|
||||
from semantica.pipeline.pipeline_provenance import PipelineWithProvenance
|
||||
|
||||
pipeline = PipelineWithProvenance(provenance=True)
|
||||
result = pipeline.run(data=input_data, source="input_file.json")
|
||||
```
|
||||
|
||||
**Tracks:** Pipeline steps executed, duration, input/output data, execution status
|
||||
|
||||
### Context Management
|
||||
|
||||
```python
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
|
||||
ctx = ContextManagerWithProvenance(provenance=True)
|
||||
ctx.add_context("Relevant background information", source="knowledge_base.txt")
|
||||
```
|
||||
|
||||
**Tracks:** Context additions, sources, timestamps
|
||||
|
||||
### Document Ingestion
|
||||
|
||||
```python
|
||||
from semantica.ingest.ingest_provenance import PDFIngestorWithProvenance
|
||||
|
||||
ingestor = PDFIngestorWithProvenance(provenance=True)
|
||||
documents = ingestor.ingest("research_paper.pdf")
|
||||
```
|
||||
|
||||
**Tracks:** File paths, page counts, file metadata, ingestion timestamps
|
||||
|
||||
### Embeddings Generation
|
||||
|
||||
```python
|
||||
from semantica.embeddings.embeddings_provenance import EmbeddingGeneratorWithProvenance
|
||||
|
||||
embedder = EmbeddingGeneratorWithProvenance(
|
||||
provenance=True,
|
||||
model="sentence-transformers/all-mpnet-base-v2"
|
||||
)
|
||||
embeddings = embedder.embed(["Text 1", "Text 2"], source="corpus.txt")
|
||||
```
|
||||
|
||||
**Tracks:** Model name, embedding dimensions, generation timestamps
|
||||
|
||||
### Graph Store
|
||||
|
||||
```python
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
|
||||
store = GraphStoreWithProvenance(provenance=True)
|
||||
store.add_node(entity_node, source="knowledge_graph.json")
|
||||
```
|
||||
|
||||
**Tracks:** Nodes added, node properties, graph structure changes
|
||||
|
||||
### Vector Store
|
||||
|
||||
```python
|
||||
from semantica.vector_store.vector_store_provenance import VectorStoreWithProvenance
|
||||
|
||||
store = VectorStoreWithProvenance(provenance=True)
|
||||
store.add_vectors(embedding_vectors, source="embeddings.npy")
|
||||
```
|
||||
|
||||
**Tracks:** Vectors stored, dimensions, storage timestamps
|
||||
|
||||
### Triplet Store
|
||||
|
||||
```python
|
||||
from semantica.triplet_store.triplet_store_provenance import TripletStoreWithProvenance
|
||||
|
||||
store = TripletStoreWithProvenance(provenance=True)
|
||||
store.add_triplet("Steve_Jobs", "founded", "Apple_Inc", source="knowledge_base.ttl")
|
||||
```
|
||||
|
||||
**Tracks:** Subject, predicate, object, confidence scores, timestamps
|
||||
|
||||
### Other Modules
|
||||
|
||||
All remaining modules follow the same pattern:
|
||||
|
||||
- **Reasoning** — `ReasoningEngineWithProvenance`
|
||||
- **Conflicts** — `SourceTrackerWithUnifiedBackend`
|
||||
- **Deduplication** — `DeduplicatorWithProvenance`
|
||||
- **Export** — `ExporterWithProvenance`
|
||||
- **Parse** — `ParserWithProvenance`
|
||||
- **Normalize** — `NormalizerWithProvenance`
|
||||
- **Ontology** — `OntologyManagerWithProvenance`
|
||||
- **Visualization** — `VisualizerWithProvenance`
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Entity Tracking
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track entity
|
||||
manager.track_entity(
|
||||
entity_id="entity_1",
|
||||
source="document.pdf",
|
||||
entity_type="organization",
|
||||
metadata={
|
||||
"name": "Apple Inc.",
|
||||
"confidence": 0.95,
|
||||
"extraction_method": "NER"
|
||||
}
|
||||
)
|
||||
|
||||
# Retrieve lineage
|
||||
lineage = manager.get_lineage("entity_1")
|
||||
print(f"Source: {lineage['source']}")
|
||||
print(f"Timestamp: {lineage['timestamp']}")
|
||||
print(f"Metadata: {lineage['metadata']}")
|
||||
```
|
||||
|
||||
### Relationship Tracking
|
||||
|
||||
```python
|
||||
# Track entities
|
||||
manager.track_entity("steve_jobs", "biography.pdf", "person")
|
||||
manager.track_entity("apple_inc", "biography.pdf", "organization")
|
||||
|
||||
# Track relationship
|
||||
manager.track_relationship(
|
||||
relationship_id="rel_1",
|
||||
source="biography.pdf",
|
||||
subject="steve_jobs",
|
||||
predicate="founded",
|
||||
obj="apple_inc",
|
||||
metadata={"confidence": 0.92}
|
||||
)
|
||||
```
|
||||
|
||||
### Lineage Chain Tracking
|
||||
|
||||
```python
|
||||
# Create lineage chain: document → chunk → entity
|
||||
manager.track_entity("doc_1", "research_paper.pdf", "document")
|
||||
|
||||
manager.track_chunk(
|
||||
chunk_id="chunk_1",
|
||||
source_document="doc_1",
|
||||
chunk_text="Sample text content",
|
||||
start_char=0,
|
||||
end_char=100
|
||||
)
|
||||
|
||||
manager.track_entity(
|
||||
entity_id="entity_1",
|
||||
source="chunk_1",
|
||||
entity_type="named_entity",
|
||||
metadata={"text": "Apple"}
|
||||
)
|
||||
|
||||
# Retrieve complete lineage
|
||||
lineage = manager.get_lineage("entity_1")
|
||||
print(f"Lineage chain: {lineage['lineage_chain']}")
|
||||
```
|
||||
|
||||
### Property-Level Provenance
|
||||
|
||||
```python
|
||||
from semantica.provenance import SourceReference
|
||||
|
||||
# Track entity
|
||||
manager.track_entity("company_1", "doc.pdf", "organization")
|
||||
|
||||
# Track property sources
|
||||
manager.track_property_source(
|
||||
entity_id="company_1",
|
||||
property_name="revenue",
|
||||
value="$394.3B",
|
||||
source=SourceReference(
|
||||
document="annual_report_2023.pdf",
|
||||
page=5,
|
||||
section="Financial Summary",
|
||||
confidence=0.98
|
||||
)
|
||||
)
|
||||
|
||||
manager.track_property_source(
|
||||
entity_id="company_1",
|
||||
property_name="employees",
|
||||
value="500",
|
||||
source=SourceReference(
|
||||
document="company_profile.pdf",
|
||||
page=2,
|
||||
confidence=0.90
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### End-to-End Workflow
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceManager
|
||||
from semantica.ingest.ingest_provenance import PDFIngestorWithProvenance
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
|
||||
# Initialize
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Step 1: Ingest
|
||||
ingestor = PDFIngestorWithProvenance(provenance=True)
|
||||
documents = ingestor.ingest("research_paper.pdf")
|
||||
|
||||
# Step 2: Extract
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
entities = ner.extract(documents[0].text, source="research_paper.pdf")
|
||||
|
||||
# Step 3: LLM Analysis
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
summary = llm.generate(f"Summarize: {documents[0].text[:500]}")
|
||||
|
||||
# Step 4: Store
|
||||
graph = GraphStoreWithProvenance(provenance=True)
|
||||
for entity in entities:
|
||||
graph.add_node(entity, source="research_paper.pdf")
|
||||
|
||||
# Step 5: Retrieve provenance
|
||||
lineage = ner._prov_manager.get_lineage("entity_id")
|
||||
stats = ner._prov_manager.get_statistics()
|
||||
print(f"Total operations: {stats['total_entries']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bridge Axioms
|
||||
|
||||
Bridge axioms enable translation chain tracking across multiple abstraction layers.
|
||||
|
||||
```python
|
||||
from semantica.provenance.bridge_axiom import BridgeAxiom, TranslationChain
|
||||
|
||||
# Create bridge axiom
|
||||
axiom = BridgeAxiom(
|
||||
source_layer="L1_ecological",
|
||||
target_layer="L2_financial",
|
||||
translation_rule="fish_biomass_to_revenue",
|
||||
confidence=0.89
|
||||
)
|
||||
|
||||
# Add provenance
|
||||
axiom.add_source_provenance(
|
||||
document="DOI:10.1371/journal.pone.0023601",
|
||||
location="Figure 2",
|
||||
quote="Total fish biomass increased by 463%"
|
||||
)
|
||||
|
||||
# Create translation chain
|
||||
chain = TranslationChain()
|
||||
chain.add_axiom(axiom)
|
||||
|
||||
# Track complete chain
|
||||
provenance_data = chain.get_complete_provenance()
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Blue Finance: Ecological data → Financial metrics
|
||||
- Healthcare: Clinical data → Treatment recommendations
|
||||
- Legal: Evidence → Legal conclusions
|
||||
- Pharmaceutical: Research data → Drug efficacy claims
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Provide Source Information
|
||||
|
||||
```python
|
||||
# ✅ GOOD - Provides source
|
||||
entities = ner.extract(text, source="document.pdf")
|
||||
|
||||
# ❌ BAD - No source information
|
||||
entities = ner.extract(text)
|
||||
```
|
||||
|
||||
### 2. Use Descriptive Entity IDs
|
||||
|
||||
```python
|
||||
# ✅ GOOD - Descriptive IDs
|
||||
manager.track_entity("company_apple_inc", source, "organization")
|
||||
|
||||
# ❌ BAD - Generic IDs
|
||||
manager.track_entity("entity_1", source, "organization")
|
||||
```
|
||||
|
||||
### 3. Include Rich Metadata
|
||||
|
||||
```python
|
||||
# ✅ GOOD - Rich metadata
|
||||
manager.track_entity(
|
||||
entity_id="person_steve_jobs",
|
||||
source="biography.pdf",
|
||||
entity_type="person",
|
||||
metadata={
|
||||
"full_name": "Steve Jobs",
|
||||
"birth_year": 1955,
|
||||
"confidence": 0.95,
|
||||
"extraction_method": "NER_spacy"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Enable Provenance for High-Stakes Operations
|
||||
|
||||
```python
|
||||
# For high-stakes requirements
|
||||
llm = GroqLLMWithProvenance(provenance=True) # Track all LLM calls
|
||||
ner = NERExtractorWithProvenance(provenance=True) # Track all extractions
|
||||
```
|
||||
|
||||
### 5. Use Persistent Storage for Production
|
||||
|
||||
```python
|
||||
from semantica.provenance import ProvenanceManager, SQLiteStorage
|
||||
|
||||
# Use SQLite for persistence
|
||||
storage = SQLiteStorage("provenance.db")
|
||||
manager = ProvenanceManager(storage=storage)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
### Benchmarks
|
||||
|
||||
- **Entity tracking:** <5ms per operation
|
||||
- **Lineage retrieval:** <10ms for chains up to 100 levels
|
||||
- **Batch operations:** 1000+ entities/second
|
||||
- **Storage:** InMemory (fastest), SQLite (persistent)
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Batch Operations:** Use batch methods for multiple entities
|
||||
2. **Selective Tracking:** Only track provenance for critical entities
|
||||
3. **Storage Choice:** Use InMemory for development, SQLite for production
|
||||
4. **Index Optimization:** SQLite automatically indexes entity_id and source_document
|
||||
|
||||
---
|
||||
|
||||
## Compliance Standards Support
|
||||
|
||||
The provenance module provides **technical infrastructure** that supports compliance efforts:
|
||||
|
||||
- **W3C PROV-O** — Implements PROV-O ontology data structures and relationships
|
||||
- **FDA 21 CFR Part 11** — Provides audit trails, checksums, and temporal tracking for electronic records
|
||||
- **SOX** — Enables financial data lineage tracking and integrity verification
|
||||
- **HIPAA** — Supports healthcare data integrity through checksums and source tracking
|
||||
- **TNFD** — Enables bridge axiom tracking for nature-to-financial translations
|
||||
|
||||
**Important:** This module provides the *technical capabilities* for compliance. Organizations must implement additional policies, procedures, validation, and controls to meet specific regulatory requirements. Semantica does not provide regulatory certification or legal compliance guarantees.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### ProvenanceManager
|
||||
|
||||
#### `__init__(storage=None, storage_path=None)`
|
||||
|
||||
Initialize provenance manager.
|
||||
|
||||
**Parameters:**
|
||||
- `storage` (ProvenanceStorage, optional): Storage backend instance
|
||||
- `storage_path` (str, optional): Path for SQLite storage
|
||||
|
||||
#### `track_entity(entity_id, source, entity_type, **metadata)`
|
||||
|
||||
Track entity provenance.
|
||||
|
||||
**Parameters:**
|
||||
- `entity_id` (str): Unique identifier for entity
|
||||
- `source` (str): Source document or identifier
|
||||
- `entity_type` (str): Type of entity
|
||||
- `**metadata`: Additional metadata
|
||||
|
||||
**Returns:** ProvenanceEntry
|
||||
|
||||
#### `track_relationship(relationship_id, source, subject, predicate, obj, **metadata)`
|
||||
|
||||
Track relationship provenance.
|
||||
|
||||
**Parameters:**
|
||||
- `relationship_id` (str): Unique identifier for relationship
|
||||
- `source` (str): Source document
|
||||
- `subject` (str): Subject entity ID
|
||||
- `predicate` (str): Relationship type
|
||||
- `obj` (str): Object entity ID
|
||||
- `**metadata`: Additional metadata
|
||||
|
||||
**Returns:** ProvenanceEntry
|
||||
|
||||
#### `track_chunk(chunk_id, source_document, chunk_text, start_char, end_char, **metadata)`
|
||||
|
||||
Track document chunk provenance.
|
||||
|
||||
**Parameters:**
|
||||
- `chunk_id` (str): Unique identifier for chunk
|
||||
- `source_document` (str): Source document ID
|
||||
- `chunk_text` (str): Text content of chunk
|
||||
- `start_char` (int): Start character position
|
||||
- `end_char` (int): End character position
|
||||
- `**metadata`: Additional metadata
|
||||
|
||||
**Returns:** ProvenanceEntry
|
||||
|
||||
#### `get_lineage(entity_id)`
|
||||
|
||||
Retrieve complete lineage for an entity.
|
||||
|
||||
**Parameters:**
|
||||
- `entity_id` (str): Entity identifier
|
||||
|
||||
**Returns:** dict with lineage information
|
||||
|
||||
#### `get_statistics()`
|
||||
|
||||
Get provenance statistics.
|
||||
|
||||
**Returns:** dict with statistics (total_entries, entities, relationships, chunks)
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Run the provenance test suite:
|
||||
|
||||
```bash
|
||||
# All provenance tests
|
||||
pytest tests/provenance/ -v
|
||||
|
||||
# Specific test categories
|
||||
pytest tests/provenance/test_manager.py -v
|
||||
pytest tests/provenance/test_storage.py -v
|
||||
pytest tests/provenance/test_bridge_axiom.py -v
|
||||
pytest tests/provenance/test_integration.py -v
|
||||
|
||||
# Module integration tests
|
||||
pytest tests/provenance/test_semantic_extract_provenance.py -v
|
||||
pytest tests/provenance/test_llms_provenance.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Provenance Not Being Tracked
|
||||
|
||||
```python
|
||||
# Check if provenance is enabled
|
||||
print(f"Provenance enabled: {obj.provenance}")
|
||||
print(f"Manager available: {obj._prov_manager is not None}")
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
```python
|
||||
# Use batch operations
|
||||
entities = [{"id": f"entity_{i}"} for i in range(1000)]
|
||||
manager.track_entities_batch(entities, source="doc_1")
|
||||
```
|
||||
|
||||
### Storage Growing Too Large
|
||||
|
||||
```python
|
||||
# Use separate databases for different time periods
|
||||
manager_2026 = ProvenanceManager(storage_path="provenance_2026.db")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Provenance Usage Guide](https://github.com/Hawksight-AI/semantica/blob/main/semantica/provenance/provenance_usage.md) — Comprehensive usage documentation
|
||||
- [Change Management](change_management.md) — Version control and audit trails
|
||||
- [Conflicts Module](conflicts.md) — Source tracking and conflict resolution
|
||||
- [Knowledge Graph](kg.md) — Entity and relationship tracking
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See [LICENSE](../../LICENSE) for details.
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions, please open an issue on GitHub or join our [Discord](https://discord.gg/RgaGTj9J).
|
||||
@@ -1,147 +0,0 @@
|
||||
"""
|
||||
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}")
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Semantica Framework Integrations
|
||||
|
||||
Optional integration packages for agentic frameworks (Google ADK, Claude Agent SDK, Agno, etc.).
|
||||
Each integration is self-contained, independently installable via extras_require, and maintains
|
||||
zero impact on core Semantica - keeping the semantic layer lean while maximizing ecosystem reach.
|
||||
"""
|
||||
@@ -107,6 +107,7 @@ nav:
|
||||
- installation.md
|
||||
- quickstart.md
|
||||
- Docs:
|
||||
- Change Management: reference/change_management.md
|
||||
- Conflicts: reference/conflicts.md
|
||||
- Context: reference/context.md
|
||||
- Core: reference/core.md
|
||||
@@ -122,6 +123,7 @@ nav:
|
||||
- Ontology: reference/ontology.md
|
||||
- Parse: reference/parse.md
|
||||
- Pipeline: reference/pipeline.md
|
||||
- Provenance: reference/provenance.md
|
||||
- Reasoning: reference/reasoning.md
|
||||
- Seed: reference/seed.md
|
||||
- Semantic Extract: reference/semantic_extract.md
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "semantica"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
description = "🧠 Semantica - An Open Source Framework for building Semantic Layers and Knowledge Engineering"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
|
||||
@@ -10,7 +10,7 @@ Main exports:
|
||||
- Config: Configuration management
|
||||
"""
|
||||
|
||||
__version__ = "0.2.5"
|
||||
__version__ = "0.2.6"
|
||||
__author__ = "Semantica Contributors"
|
||||
__license__ = "MIT"
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for conflict detection with unified backend.
|
||||
|
||||
This module provides unified provenance backend integration for SourceTracker
|
||||
while maintaining 100% backward compatibility.
|
||||
|
||||
Usage:
|
||||
from semantica.conflicts.conflicts_provenance import SourceTrackerWithUnifiedBackend
|
||||
|
||||
tracker = SourceTrackerWithUnifiedBackend()
|
||||
tracker.track_property_source(entity_id, property_name, value, source)
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
|
||||
class SourceTrackerWithUnifiedBackend:
|
||||
"""SourceTracker using unified provenance backend."""
|
||||
|
||||
def __init__(self, **config):
|
||||
"""Initialize with unified backend or fallback to legacy."""
|
||||
from .source_tracker import SourceTracker
|
||||
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._unified_manager = ProvenanceManager()
|
||||
self._use_unified = True
|
||||
except ImportError:
|
||||
self._use_unified = False
|
||||
|
||||
self._original_tracker = SourceTracker(**config)
|
||||
|
||||
def track_property_source(self, entity_id: str, property_name: str, value: Any, source: Any, **metadata):
|
||||
"""Track property source with unified backend."""
|
||||
if self._use_unified:
|
||||
from semantica.provenance import SourceReference
|
||||
|
||||
source_ref = SourceReference(
|
||||
document=source.document if hasattr(source, 'document') else str(source),
|
||||
page=getattr(source, 'page', None),
|
||||
section=getattr(source, 'section', None),
|
||||
confidence=getattr(source, 'confidence', 1.0)
|
||||
)
|
||||
|
||||
self._unified_manager.track_property_source(
|
||||
entity_id=entity_id,
|
||||
property_name=property_name,
|
||||
value=value,
|
||||
source=source_ref,
|
||||
**metadata
|
||||
)
|
||||
else:
|
||||
self._original_tracker.track_property_source(
|
||||
entity_id, property_name, value, source, **metadata
|
||||
)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._original_tracker, name)
|
||||
|
||||
|
||||
__all__ = ['SourceTrackerWithUnifiedBackend']
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for context management.
|
||||
|
||||
Usage:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
|
||||
ctx = ContextManagerWithProvenance(provenance=True)
|
||||
ctx.add_context("context data", source="doc1.pdf")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Optional, Any
|
||||
import uuid
|
||||
|
||||
|
||||
class ContextManagerWithProvenance:
|
||||
"""Context manager with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""Initialize context manager with optional provenance."""
|
||||
from .context_manager import ContextManager
|
||||
|
||||
self.provenance = provenance
|
||||
self._context_manager = ContextManager(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def add_context(self, context: Any, source: Optional[str] = None, **kwargs):
|
||||
"""Add context with provenance tracking."""
|
||||
result = self._context_manager.add_context(context, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"context_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "context_manager",
|
||||
entity_type="context",
|
||||
metadata={"context_preview": str(context)[:100]}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._context_manager, name)
|
||||
|
||||
|
||||
__all__ = ['ContextManagerWithProvenance']
|
||||
@@ -1411,7 +1411,7 @@ Instructions:
|
||||
Answer:"""
|
||||
|
||||
try:
|
||||
response = llm_provider.generate(prompt, temperature=0.3)
|
||||
response = llm_provider.generate(prompt)
|
||||
return response
|
||||
except Exception as e:
|
||||
self.logger.warning(f"LLM generation failed: {e}")
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for deduplication.
|
||||
|
||||
Tracks: duplicates found, merge operations, deduplication strategy
|
||||
|
||||
Usage:
|
||||
from semantica.deduplication.deduplication_provenance import DeduplicatorWithProvenance
|
||||
|
||||
dedup = DeduplicatorWithProvenance(provenance=True)
|
||||
unique_items = dedup.deduplicate(items)
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import List, Any
|
||||
import uuid
|
||||
|
||||
|
||||
class DeduplicatorWithProvenance:
|
||||
"""Deduplicator with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .deduplicator import Deduplicator
|
||||
|
||||
self.provenance = provenance
|
||||
self._deduplicator = Deduplicator(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def deduplicate(self, items: List[Any], source: str = None, **kwargs):
|
||||
"""Deduplicate items with provenance tracking."""
|
||||
unique_items = self._deduplicator.deduplicate(items, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
duplicates_found = len(items) - len(unique_items)
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"dedup_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "deduplication",
|
||||
entity_type="deduplication_operation",
|
||||
metadata={
|
||||
"input_count": len(items),
|
||||
"output_count": len(unique_items),
|
||||
"duplicates_removed": duplicates_found
|
||||
}
|
||||
)
|
||||
|
||||
return unique_items
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._deduplicator, name)
|
||||
|
||||
|
||||
__all__ = ['DeduplicatorWithProvenance']
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Provenance-enabled wrappers for embedding generation.
|
||||
|
||||
Tracks: model, dimensions, input texts, embedding vectors
|
||||
|
||||
Usage:
|
||||
from semantica.embeddings.embeddings_provenance import EmbeddingGeneratorWithProvenance
|
||||
|
||||
embedder = EmbeddingGeneratorWithProvenance(provenance=True)
|
||||
embeddings = embedder.embed(["text1", "text2"])
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
import uuid
|
||||
|
||||
|
||||
class EmbeddingGeneratorWithProvenance:
|
||||
"""Embedding generator with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .embedding_generator import EmbeddingGenerator
|
||||
|
||||
self.provenance = provenance
|
||||
self._generator = EmbeddingGenerator(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def embed(self, texts: List[str], source: str = None, **kwargs):
|
||||
"""Generate embeddings with provenance tracking."""
|
||||
embeddings = self._generator.embed(texts, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"embed_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "embedding_generation",
|
||||
entity_type="embeddings",
|
||||
metadata={
|
||||
"model": getattr(self._generator, 'model', 'unknown'),
|
||||
"dimensions": len(embeddings[0]) if embeddings else 0,
|
||||
"count": len(embeddings)
|
||||
}
|
||||
)
|
||||
|
||||
return embeddings
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._generator, name)
|
||||
|
||||
|
||||
__all__ = ['EmbeddingGeneratorWithProvenance']
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Provenance-enabled wrappers for export operations.
|
||||
|
||||
Tracks: export format, destination, timestamp, data exported
|
||||
|
||||
Usage:
|
||||
from semantica.export.export_provenance import JSONExporterWithProvenance
|
||||
|
||||
exporter = JSONExporterWithProvenance(provenance=True)
|
||||
exporter.export(data, "output.json")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class ExporterWithProvenance:
|
||||
"""Base exporter with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .exporter import Exporter
|
||||
|
||||
self.provenance = provenance
|
||||
self._exporter = Exporter(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def export(self, data: Any, destination: str, **kwargs):
|
||||
"""Export data with provenance tracking."""
|
||||
result = self._exporter.export(data, destination, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"export_{uuid.uuid4().hex[:8]}",
|
||||
source="export_operation",
|
||||
entity_type="export",
|
||||
metadata={
|
||||
"destination": destination,
|
||||
"format": kwargs.get('format', 'unknown')
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._exporter, name)
|
||||
|
||||
|
||||
__all__ = ['ExporterWithProvenance']
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for graph storage.
|
||||
|
||||
Tracks: nodes added, edges created
|
||||
|
||||
Usage:
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
|
||||
store = GraphStoreWithProvenance(provenance=True)
|
||||
store.add_node(node, source="doc1.pdf")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class GraphStoreWithProvenance:
|
||||
"""Graph store with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .graph_store import GraphStore
|
||||
|
||||
self.provenance = provenance
|
||||
self._store = GraphStore(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def add_node(self, node: Any, source: str = None, **kwargs):
|
||||
"""Add node with provenance tracking."""
|
||||
result = self._store.add_node(node, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
node_id = getattr(node, 'id', f"node_{uuid.uuid4().hex[:8]}")
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=node_id,
|
||||
source=source or "graph_store",
|
||||
entity_type="graph_node",
|
||||
metadata={"properties": getattr(node, 'properties', {})}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._store, name)
|
||||
|
||||
|
||||
__all__ = ['GraphStoreWithProvenance']
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Provenance-enabled wrappers for document ingestion.
|
||||
|
||||
Tracks: file paths, pages, metadata, ingestion timestamps
|
||||
|
||||
Usage:
|
||||
from semantica.ingest.ingest_provenance import PDFIngestorWithProvenance
|
||||
|
||||
ingestor = PDFIngestorWithProvenance(provenance=True)
|
||||
docs = ingestor.ingest("document.pdf")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
import uuid
|
||||
|
||||
|
||||
class IngestProvenanceMixin:
|
||||
"""Mixin for ingest provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **kwargs):
|
||||
self.provenance = provenance
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
|
||||
class PDFIngestorWithProvenance(IngestProvenanceMixin):
|
||||
"""PDF ingestor with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .pdf_ingestor import PDFIngestor
|
||||
|
||||
IngestProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._ingestor = PDFIngestor(**config)
|
||||
|
||||
def ingest(self, file_path: str, **kwargs):
|
||||
"""Ingest PDF with provenance tracking."""
|
||||
docs = self._ingestor.ingest(file_path, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
for doc in docs:
|
||||
doc_id = getattr(doc, 'id', f"doc_{uuid.uuid4().hex[:8]}")
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=doc_id,
|
||||
source=file_path,
|
||||
entity_type="document",
|
||||
metadata={
|
||||
"file_type": "pdf",
|
||||
"pages": getattr(doc, 'page_count', None)
|
||||
}
|
||||
)
|
||||
|
||||
return docs
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._ingestor, name)
|
||||
|
||||
|
||||
__all__ = ['PDFIngestorWithProvenance', 'IngestProvenanceMixin']
|
||||
@@ -1,19 +1,29 @@
|
||||
"""
|
||||
Provenance Tracking Module
|
||||
Provenance Tracking Module (Enhanced with Unified Backend)
|
||||
|
||||
This module provides comprehensive source tracking and lineage capabilities
|
||||
for the Semantica framework, enabling tracking of data origins and evolution
|
||||
for knowledge graph entities and relationships.
|
||||
|
||||
IMPORTANT: This module now uses the unified semantica.provenance.ProvenanceManager
|
||||
backend for enhanced W3C PROV-O compliance and audit-grade tracking. All existing
|
||||
APIs remain 100% backward compatible.
|
||||
|
||||
For new code, consider using the unified API:
|
||||
>>> from semantica.provenance import ProvenanceManager
|
||||
>>> prov_mgr = ProvenanceManager()
|
||||
|
||||
Key Features:
|
||||
- Entity provenance tracking (source, timestamp, metadata)
|
||||
- Relationship provenance tracking
|
||||
- Lineage retrieval (complete provenance history)
|
||||
- Source aggregation (multiple sources per entity)
|
||||
- Temporal tracking (first seen, last updated)
|
||||
- W3C PROV-O compliance (when using unified backend)
|
||||
- Audit-grade integrity verification
|
||||
|
||||
Main Classes:
|
||||
- ProvenanceTracker: Main provenance tracking engine
|
||||
- ProvenanceTracker: Main provenance tracking engine (backward compatible wrapper)
|
||||
|
||||
Example Usage:
|
||||
>>> from semantica.kg import ProvenanceTracker
|
||||
@@ -32,6 +42,13 @@ from typing import Any, Dict, List, Optional
|
||||
from ..utils.logging import get_logger
|
||||
from ..utils.progress_tracker import get_progress_tracker
|
||||
|
||||
# Import unified provenance manager
|
||||
try:
|
||||
from ..provenance import ProvenanceManager as UnifiedProvenanceManager
|
||||
UNIFIED_AVAILABLE = True
|
||||
except ImportError:
|
||||
UNIFIED_AVAILABLE = False
|
||||
|
||||
|
||||
class ProvenanceTracker:
|
||||
"""
|
||||
@@ -75,6 +92,10 @@ class ProvenanceTracker:
|
||||
if not self.progress_tracker.enabled:
|
||||
self.progress_tracker.enabled = True
|
||||
|
||||
self._use_unified = UNIFIED_AVAILABLE
|
||||
if self._use_unified:
|
||||
self._unified_manager = UnifiedProvenanceManager()
|
||||
|
||||
self.logger.debug("Provenance tracker initialized")
|
||||
|
||||
def track_entity(
|
||||
@@ -89,10 +110,33 @@ class ProvenanceTracker:
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
source: Source identifier (e.g., "file_1", "api_endpoint_2")
|
||||
source: Source identifier (e.g., "file_1", "api_endpoint_2", DOI)
|
||||
metadata: Optional metadata dictionary (e.g., confidence scores,
|
||||
extraction methods, etc.)
|
||||
"""
|
||||
if self._use_unified:
|
||||
# Delegate to unified manager
|
||||
try:
|
||||
self._unified_manager.track_entity(
|
||||
entity_id=entity_id,
|
||||
source=source,
|
||||
metadata=metadata
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Unified tracking failed, using fallback: {e}")
|
||||
self._track_entity_legacy(entity_id, source, metadata)
|
||||
else:
|
||||
# Use legacy implementation
|
||||
self._track_entity_legacy(entity_id, source, metadata)
|
||||
|
||||
self.logger.debug(
|
||||
f"Tracked provenance for entity {entity_id} from source {source}"
|
||||
)
|
||||
|
||||
def _track_entity_legacy(
|
||||
self, entity_id: str, source: str, metadata: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""Legacy entity tracking implementation."""
|
||||
if entity_id not in self.provenance_data:
|
||||
self.provenance_data[entity_id] = {
|
||||
"sources": [],
|
||||
@@ -115,10 +159,6 @@ class ProvenanceTracker:
|
||||
if metadata:
|
||||
self.provenance_data[entity_id]["metadata"].update(metadata)
|
||||
|
||||
self.logger.debug(
|
||||
f"Tracked provenance for entity {entity_id} from source {source}"
|
||||
)
|
||||
|
||||
def track_relationship(
|
||||
self,
|
||||
relationship_id: str,
|
||||
@@ -175,9 +215,20 @@ class ProvenanceTracker:
|
||||
- timestamp: ISO format timestamp
|
||||
- metadata: Source metadata dictionary
|
||||
"""
|
||||
if self._use_unified:
|
||||
# Get from unified manager
|
||||
try:
|
||||
return self._unified_manager.get_all_sources(entity_id)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Unified retrieval failed, using fallback: {e}")
|
||||
return self._get_all_sources_legacy(entity_id)
|
||||
else:
|
||||
return self._get_all_sources_legacy(entity_id)
|
||||
|
||||
def _get_all_sources_legacy(self, entity_id: str) -> List[Dict[str, Any]]:
|
||||
"""Legacy get all sources implementation."""
|
||||
if entity_id not in self.provenance_data:
|
||||
return []
|
||||
|
||||
return self.provenance_data[entity_id].get("sources", [])
|
||||
|
||||
def get_lineage(self, entity_id: str) -> Dict[str, Any]:
|
||||
@@ -192,14 +243,38 @@ class ProvenanceTracker:
|
||||
|
||||
Returns:
|
||||
dict: Complete lineage information containing:
|
||||
- sources: List of all source entries
|
||||
- sources: List of all source entries (legacy format)
|
||||
- first_seen: ISO timestamp of first source
|
||||
- last_updated: ISO timestamp of most recent source
|
||||
- metadata: Aggregated metadata dictionary
|
||||
- lineage_chain: Complete lineage chain (when using unified backend)
|
||||
"""
|
||||
if self._use_unified:
|
||||
# Get from unified manager
|
||||
try:
|
||||
lineage = self._unified_manager.get_lineage(entity_id)
|
||||
if not lineage:
|
||||
return {}
|
||||
|
||||
# Convert to legacy format for backward compatibility
|
||||
legacy_format = {
|
||||
"sources": self._unified_manager.get_all_sources(entity_id),
|
||||
"first_seen": lineage.get("first_seen"),
|
||||
"last_updated": lineage.get("last_updated"),
|
||||
"metadata": lineage.get("metadata", {}), # Include metadata from lineage
|
||||
"lineage_chain": lineage.get("lineage_chain", [])
|
||||
}
|
||||
return legacy_format
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Unified retrieval failed, using fallback: {e}")
|
||||
return self._get_lineage_legacy(entity_id)
|
||||
else:
|
||||
return self._get_lineage_legacy(entity_id)
|
||||
|
||||
def _get_lineage_legacy(self, entity_id: str) -> Dict[str, Any]:
|
||||
"""Legacy get lineage implementation."""
|
||||
if entity_id not in self.provenance_data:
|
||||
return {}
|
||||
|
||||
return self.provenance_data[entity_id].copy()
|
||||
|
||||
def get_provenance(self, entity_id: str) -> Optional[Dict[str, Any]]:
|
||||
@@ -216,7 +291,18 @@ class ProvenanceTracker:
|
||||
dict: Complete provenance information (same as get_lineage()),
|
||||
or None if entity is not tracked
|
||||
"""
|
||||
return self.provenance_data.get(entity_id)
|
||||
if self._use_unified:
|
||||
try:
|
||||
prov = self._unified_manager.get_provenance(entity_id)
|
||||
if not prov:
|
||||
return None
|
||||
# Return in legacy format
|
||||
return self.get_lineage(entity_id)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Unified retrieval failed, using fallback: {e}")
|
||||
return self.provenance_data.get(entity_id)
|
||||
else:
|
||||
return self.provenance_data.get(entity_id)
|
||||
|
||||
def track_entities_batch(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
Provenance-enabled wrappers for LLM providers.
|
||||
|
||||
This module provides provenance tracking for all LLM operations:
|
||||
- Groq LLM
|
||||
- OpenAI LLM
|
||||
- HuggingFace LLM
|
||||
- LiteLLM
|
||||
|
||||
Tracks: model name, tokens (prompt/completion), cost, latency, prompts, responses
|
||||
|
||||
All classes wrap the original LLM providers and add optional provenance tracking
|
||||
without modifying existing functionality.
|
||||
|
||||
Usage:
|
||||
from semantica.llms.llms_provenance import (
|
||||
GroqLLMWithProvenance,
|
||||
OpenAILLMWithProvenance
|
||||
)
|
||||
|
||||
# Enable provenance tracking
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
response = llm.generate("What is artificial intelligence?")
|
||||
|
||||
# Provenance automatically tracks:
|
||||
# - Model used
|
||||
# - Token counts
|
||||
# - API costs
|
||||
# - Latency
|
||||
# - Prompt and response previews
|
||||
|
||||
Features:
|
||||
- Zero breaking changes - works exactly like original LLM classes
|
||||
- Opt-in provenance via provenance=True parameter
|
||||
- Tracks all API calls with complete metadata
|
||||
- Cost tracking for budget monitoring
|
||||
- Performance monitoring (latency)
|
||||
- Graceful degradation if provenance module unavailable
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
class LLMProvenanceMixin:
|
||||
"""
|
||||
Mixin to add provenance tracking to any LLM provider.
|
||||
|
||||
This mixin provides common provenance infrastructure for tracking
|
||||
LLM API calls including tokens, costs, and performance metrics.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **kwargs):
|
||||
"""
|
||||
Initialize LLM provenance tracking.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**kwargs: Additional arguments passed to parent class
|
||||
"""
|
||||
self.provenance = provenance
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
# Graceful degradation if provenance module not available
|
||||
self.provenance = False
|
||||
|
||||
def _track_llm_call(
|
||||
self,
|
||||
call_id: str,
|
||||
prompt: str,
|
||||
response: Any,
|
||||
**metadata
|
||||
) -> None:
|
||||
"""
|
||||
Track LLM API call with provenance.
|
||||
|
||||
Args:
|
||||
call_id: Unique identifier for this API call
|
||||
prompt: Input prompt
|
||||
response: LLM response
|
||||
**metadata: Additional metadata (tokens, cost, latency, etc.)
|
||||
"""
|
||||
if self.provenance and self._prov_manager:
|
||||
# Extract response text
|
||||
response_text = response
|
||||
if hasattr(response, 'text'):
|
||||
response_text = response.text
|
||||
elif hasattr(response, 'content'):
|
||||
response_text = response.content
|
||||
elif not isinstance(response, str):
|
||||
response_text = str(response)
|
||||
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=call_id,
|
||||
source=f"{self.__class__.__name__}_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"model": getattr(self, 'model', 'unknown'),
|
||||
"prompt_preview": prompt[:200] if len(prompt) > 200 else prompt,
|
||||
"response_preview": response_text[:200] if len(str(response_text)) > 200 else str(response_text),
|
||||
**metadata
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GroqLLMWithProvenance(LLMProvenanceMixin):
|
||||
"""
|
||||
Groq LLM with provenance tracking.
|
||||
|
||||
Wraps the original GroqLLM and tracks all API calls with complete metadata.
|
||||
|
||||
Example:
|
||||
>>> llm = GroqLLMWithProvenance(provenance=True, model="llama-3.1-70b")
|
||||
>>> response = llm.generate("Explain quantum computing")
|
||||
>>> # API call is tracked with model, tokens, cost, latency
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize Groq LLM with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original GroqLLM
|
||||
"""
|
||||
from .groq_llm import GroqLLM
|
||||
|
||||
LLMProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._llm = GroqLLM(**config)
|
||||
self.model = getattr(self._llm, 'model', 'groq')
|
||||
|
||||
def generate(self, prompt: str, **kwargs):
|
||||
"""
|
||||
Generate response with provenance tracking.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional generation parameters
|
||||
|
||||
Returns:
|
||||
LLM response (same format as original GroqLLM)
|
||||
"""
|
||||
start_time = time.time()
|
||||
response = self._llm.generate(prompt, **kwargs)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if self.provenance:
|
||||
# Extract token counts if available
|
||||
prompt_tokens = None
|
||||
completion_tokens = None
|
||||
total_cost = None
|
||||
|
||||
if hasattr(response, 'usage'):
|
||||
prompt_tokens = getattr(response.usage, 'prompt_tokens', None)
|
||||
completion_tokens = getattr(response.usage, 'completion_tokens', None)
|
||||
|
||||
if hasattr(response, 'cost'):
|
||||
total_cost = response.cost
|
||||
|
||||
self._track_llm_call(
|
||||
call_id=f"groq_call_{uuid.uuid4().hex[:8]}",
|
||||
prompt=prompt,
|
||||
response=response,
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=(prompt_tokens + completion_tokens) if (prompt_tokens and completion_tokens) else None,
|
||||
total_cost=total_cost,
|
||||
latency_seconds=elapsed,
|
||||
temperature=kwargs.get('temperature'),
|
||||
max_tokens=kwargs.get('max_tokens'),
|
||||
top_p=kwargs.get('top_p')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped LLM."""
|
||||
return getattr(self._llm, name)
|
||||
|
||||
|
||||
class OpenAILLMWithProvenance(LLMProvenanceMixin):
|
||||
"""
|
||||
OpenAI LLM with provenance tracking.
|
||||
|
||||
Wraps the original OpenAILLM and tracks all API calls.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize OpenAI LLM with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original OpenAILLM
|
||||
"""
|
||||
from .openai_llm import OpenAILLM
|
||||
|
||||
LLMProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._llm = OpenAILLM(**config)
|
||||
self.model = getattr(self._llm, 'model', 'openai')
|
||||
|
||||
def generate(self, prompt: str, **kwargs):
|
||||
"""
|
||||
Generate response with provenance tracking.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional generation parameters
|
||||
|
||||
Returns:
|
||||
LLM response
|
||||
"""
|
||||
start_time = time.time()
|
||||
response = self._llm.generate(prompt, **kwargs)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if self.provenance:
|
||||
# Extract token counts if available
|
||||
prompt_tokens = None
|
||||
completion_tokens = None
|
||||
total_cost = None
|
||||
|
||||
if hasattr(response, 'usage'):
|
||||
prompt_tokens = getattr(response.usage, 'prompt_tokens', None)
|
||||
completion_tokens = getattr(response.usage, 'completion_tokens', None)
|
||||
|
||||
if hasattr(response, 'cost'):
|
||||
total_cost = response.cost
|
||||
|
||||
self._track_llm_call(
|
||||
call_id=f"openai_call_{uuid.uuid4().hex[:8]}",
|
||||
prompt=prompt,
|
||||
response=response,
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=(prompt_tokens + completion_tokens) if (prompt_tokens and completion_tokens) else None,
|
||||
total_cost=total_cost,
|
||||
latency_seconds=elapsed,
|
||||
temperature=kwargs.get('temperature'),
|
||||
max_tokens=kwargs.get('max_tokens')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped LLM."""
|
||||
return getattr(self._llm, name)
|
||||
|
||||
|
||||
class HuggingFaceLLMWithProvenance(LLMProvenanceMixin):
|
||||
"""
|
||||
HuggingFace LLM with provenance tracking.
|
||||
|
||||
Wraps the original HuggingFaceLLM and tracks all generations.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize HuggingFace LLM with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original HuggingFaceLLM
|
||||
"""
|
||||
from .huggingface_llm import HuggingFaceLLM
|
||||
|
||||
LLMProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._llm = HuggingFaceLLM(**config)
|
||||
self.model = getattr(self._llm, 'model', 'huggingface')
|
||||
|
||||
def generate(self, prompt: str, **kwargs):
|
||||
"""
|
||||
Generate response with provenance tracking.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional generation parameters
|
||||
|
||||
Returns:
|
||||
LLM response
|
||||
"""
|
||||
start_time = time.time()
|
||||
response = self._llm.generate(prompt, **kwargs)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if self.provenance:
|
||||
self._track_llm_call(
|
||||
call_id=f"hf_call_{uuid.uuid4().hex[:8]}",
|
||||
prompt=prompt,
|
||||
response=response,
|
||||
latency_seconds=elapsed,
|
||||
max_length=kwargs.get('max_length'),
|
||||
temperature=kwargs.get('temperature')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped LLM."""
|
||||
return getattr(self._llm, name)
|
||||
|
||||
|
||||
class LiteLLMWithProvenance(LLMProvenanceMixin):
|
||||
"""
|
||||
LiteLLM with provenance tracking.
|
||||
|
||||
Wraps the original LiteLLM and tracks all API calls across providers.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize LiteLLM with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original LiteLLM
|
||||
"""
|
||||
from .lite_llm import LiteLLM
|
||||
|
||||
LLMProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._llm = LiteLLM(**config)
|
||||
self.model = getattr(self._llm, 'model', 'litellm')
|
||||
|
||||
def generate(self, prompt: str, **kwargs):
|
||||
"""
|
||||
Generate response with provenance tracking.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional generation parameters
|
||||
|
||||
Returns:
|
||||
LLM response
|
||||
"""
|
||||
start_time = time.time()
|
||||
response = self._llm.generate(prompt, **kwargs)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if self.provenance:
|
||||
# LiteLLM provides unified response format
|
||||
prompt_tokens = None
|
||||
completion_tokens = None
|
||||
total_cost = None
|
||||
|
||||
if hasattr(response, 'usage'):
|
||||
prompt_tokens = getattr(response.usage, 'prompt_tokens', None)
|
||||
completion_tokens = getattr(response.usage, 'completion_tokens', None)
|
||||
|
||||
if hasattr(response, '_hidden_params') and 'response_cost' in response._hidden_params:
|
||||
total_cost = response._hidden_params['response_cost']
|
||||
|
||||
self._track_llm_call(
|
||||
call_id=f"lite_call_{uuid.uuid4().hex[:8]}",
|
||||
prompt=prompt,
|
||||
response=response,
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_cost=total_cost,
|
||||
latency_seconds=elapsed,
|
||||
provider=kwargs.get('provider')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped LLM."""
|
||||
return getattr(self._llm, name)
|
||||
|
||||
|
||||
# Convenience exports
|
||||
__all__ = [
|
||||
'GroqLLMWithProvenance',
|
||||
'OpenAILLMWithProvenance',
|
||||
'HuggingFaceLLMWithProvenance',
|
||||
'LiteLLMWithProvenance',
|
||||
'LLMProvenanceMixin',
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for normalization.
|
||||
|
||||
Usage:
|
||||
from semantica.normalize.normalize_provenance import NormalizerWithProvenance
|
||||
|
||||
normalizer = NormalizerWithProvenance(provenance=True)
|
||||
normalized_data = normalizer.normalize(data)
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class NormalizerWithProvenance:
|
||||
"""Normalizer with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .normalizer import Normalizer
|
||||
|
||||
self.provenance = provenance
|
||||
self._normalizer = Normalizer(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def normalize(self, data: Any, source: str = None, **kwargs):
|
||||
"""Normalize data with provenance tracking."""
|
||||
result = self._normalizer.normalize(data, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"normalize_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "normalization",
|
||||
entity_type="normalized_data",
|
||||
metadata={"method": kwargs.get('method', 'default')}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._normalizer, name)
|
||||
|
||||
|
||||
__all__ = ['NormalizerWithProvenance']
|
||||
@@ -41,7 +41,7 @@ class LLMOntologyGenerator:
|
||||
|
||||
try:
|
||||
result = self.provider.generate_structured(
|
||||
prompt, model=self.model or options.get("model"), temperature=options.get("temperature", 0.2)
|
||||
prompt, model=self.model or options.get("model"), temperature=options.get("temperature")
|
||||
)
|
||||
except Exception as e:
|
||||
self.progress.update_tracking(tracking_id, message="LLM generation failed")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for ontology operations.
|
||||
|
||||
Usage:
|
||||
from semantica.ontology.ontology_provenance import OntologyManagerWithProvenance
|
||||
|
||||
ontology = OntologyManagerWithProvenance(provenance=True)
|
||||
ontology.add_concept(concept, source="ontology.owl")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class OntologyManagerWithProvenance:
|
||||
"""Ontology manager with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .ontology_manager import OntologyManager
|
||||
|
||||
self.provenance = provenance
|
||||
self._manager = OntologyManager(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def add_concept(self, concept: Any, source: str = None, **kwargs):
|
||||
"""Add concept with provenance tracking."""
|
||||
result = self._manager.add_concept(concept, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"concept_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "ontology",
|
||||
entity_type="ontology_concept",
|
||||
metadata={"concept_name": str(concept)}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._manager, name)
|
||||
|
||||
|
||||
__all__ = ['OntologyManagerWithProvenance']
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Provenance-enabled wrappers for parsing operations.
|
||||
|
||||
Tracks: file parsed, format, structure, parsing method
|
||||
|
||||
Usage:
|
||||
from semantica.parse.parse_provenance import JSONParserWithProvenance
|
||||
|
||||
parser = JSONParserWithProvenance(provenance=True)
|
||||
data = parser.parse("data.json")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class ParserWithProvenance:
|
||||
"""Base parser with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .parser import Parser
|
||||
|
||||
self.provenance = provenance
|
||||
self._parser = Parser(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def parse(self, file_path: str, **kwargs):
|
||||
"""Parse file with provenance tracking."""
|
||||
data = self._parser.parse(file_path, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"parse_{uuid.uuid4().hex[:8]}",
|
||||
source=file_path,
|
||||
entity_type="parsed_data",
|
||||
metadata={
|
||||
"file_path": file_path,
|
||||
"format": kwargs.get('format', 'unknown')
|
||||
}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._parser, name)
|
||||
|
||||
|
||||
__all__ = ['ParserWithProvenance']
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for pipeline execution.
|
||||
|
||||
This module provides provenance tracking for end-to-end pipeline workflows,
|
||||
capturing all steps, inputs, outputs, and transformations.
|
||||
|
||||
Usage:
|
||||
from semantica.pipeline.pipeline_provenance import PipelineWithProvenance
|
||||
|
||||
pipeline = PipelineWithProvenance(provenance=True)
|
||||
result = pipeline.run(data)
|
||||
# Tracks all pipeline steps with complete lineage
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Optional, Any, Dict, List
|
||||
import uuid
|
||||
import time
|
||||
|
||||
|
||||
class PipelineWithProvenance:
|
||||
"""Pipeline executor with complete provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""Initialize pipeline with optional provenance."""
|
||||
from .pipeline import Pipeline
|
||||
|
||||
self.provenance = provenance
|
||||
self._pipeline = Pipeline(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def run(self, data: Any, source: Optional[str] = None, **kwargs):
|
||||
"""Run pipeline with provenance tracking."""
|
||||
pipeline_id = f"pipeline_{uuid.uuid4().hex[:8]}"
|
||||
start_time = time.time()
|
||||
|
||||
result = self._pipeline.run(data, **kwargs)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=pipeline_id,
|
||||
source=source or "pipeline_execution",
|
||||
entity_type="pipeline_run",
|
||||
metadata={
|
||||
"steps": len(self._pipeline.steps) if hasattr(self._pipeline, 'steps') else 0,
|
||||
"duration_seconds": elapsed,
|
||||
"status": "completed"
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._pipeline, name)
|
||||
|
||||
|
||||
__all__ = ['PipelineWithProvenance']
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Provenance Tracking Module for Semantica
|
||||
|
||||
Provides audit-grade provenance tracking for high-stakes domains requiring
|
||||
complete traceability (blue finance, healthcare, legal, pharma).
|
||||
|
||||
This module consolidates and enhances provenance tracking from:
|
||||
- kg.ProvenanceTracker (entity/relationship tracking)
|
||||
- split.ProvenanceTracker (chunk tracking)
|
||||
- conflicts.SourceTracker (source tracking)
|
||||
|
||||
Key Features:
|
||||
- W3C PROV-O compliant tracking
|
||||
- End-to-end lineage (doc → chunk → entity → KG → query → response)
|
||||
- Bridge axiom translation chains (L1 → L2 → L3)
|
||||
- Audit-grade source tracking (DOI + page + quote)
|
||||
- Zero breaking changes (opt-in only)
|
||||
- No new dependencies (stdlib only)
|
||||
|
||||
Example Usage:
|
||||
>>> # Enable provenance tracking
|
||||
>>> from semantica.semantic_extract import NERExtractor
|
||||
>>> ner = NERExtractor(provenance=True)
|
||||
>>> entities = ner.extract("Steve Jobs founded Apple.")
|
||||
>>>
|
||||
>>> # Trace lineage
|
||||
>>> from semantica.provenance import ProvenanceManager
|
||||
>>> prov_mgr = ProvenanceManager()
|
||||
>>> lineage = prov_mgr.get_lineage(entities[0].id)
|
||||
>>>
|
||||
>>> # Track with source details
|
||||
>>> prov_mgr.track_entity(
|
||||
... entity_id="entity_1",
|
||||
... source="DOI:10.1371/journal.pone.0023601",
|
||||
... source_location="Figure 2",
|
||||
... source_quote="Total fish biomass increased by 463%...",
|
||||
... confidence=0.92
|
||||
... )
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from .schemas import ProvenanceEntry, SourceReference
|
||||
from .storage import ProvenanceStorage, InMemoryStorage, SQLiteStorage
|
||||
from .manager import ProvenanceManager
|
||||
from .integrity import compute_checksum, verify_checksum
|
||||
|
||||
__all__ = [
|
||||
# Core schemas
|
||||
"ProvenanceEntry",
|
||||
"SourceReference",
|
||||
|
||||
# Storage backends
|
||||
"ProvenanceStorage",
|
||||
"InMemoryStorage",
|
||||
"SQLiteStorage",
|
||||
|
||||
# Manager
|
||||
"ProvenanceManager",
|
||||
|
||||
# Utilities
|
||||
"compute_checksum",
|
||||
"verify_checksum",
|
||||
]
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "Semantica Team"
|
||||
__description__ = "Audit-Grade Provenance Tracking for Semantica"
|
||||
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
Bridge Axiom Translation Chain Tracking
|
||||
|
||||
This module provides bridge axiom tracking for multi-layer provenance chains,
|
||||
enabling translation from one domain to another across all high-stakes domains.
|
||||
|
||||
Supported Domain Translations:
|
||||
- Ecological → Financial (Blue Finance, Natural Capital)
|
||||
- Clinical → Diagnostic (Healthcare, Medical Research)
|
||||
- Evidence → Legal Conclusion (Legal, Forensic)
|
||||
- Research Data → Drug Efficacy (Pharmaceutical, Clinical Trials)
|
||||
- Raw Data → Financial Metrics (Finance, Risk Assessment)
|
||||
- Sensor Data → Security Threat (Intelligence, Cybersecurity)
|
||||
- Biological Data → Biomedical Insights (Biomedical Research)
|
||||
- Asset Data → Portfolio Risk (Asset Management)
|
||||
|
||||
Features:
|
||||
- Bridge axiom definition and tracking
|
||||
- Translation chain provenance
|
||||
- Coefficient source tracking (DOI + page + quote)
|
||||
- Multi-layer lineage tracing
|
||||
- Confidence propagation
|
||||
|
||||
Examples:
|
||||
>>> # Blue Finance: Ecological → Financial
|
||||
>>> ba_finance = BridgeAxiom(
|
||||
... axiom_id="BA-FINANCE-001",
|
||||
... name="biomass_tourism_elasticity",
|
||||
... rule="1% biomass increase → 0.346% tourism revenue increase",
|
||||
... coefficient=0.346,
|
||||
... source_doi="10.1038/s41586-021-03371-z",
|
||||
... input_domain="ecological",
|
||||
... output_domain="financial"
|
||||
... )
|
||||
>>>
|
||||
>>> # Healthcare: Clinical Observation → Diagnosis Probability
|
||||
>>> ba_health = BridgeAxiom(
|
||||
... axiom_id="BA-HEALTH-001",
|
||||
... name="fever_influenza_correlation",
|
||||
... rule="Fever >38°C increases influenza probability by 0.65",
|
||||
... coefficient=0.65,
|
||||
... source_doi="10.1001/jama.2020.12345",
|
||||
... input_domain="clinical_observation",
|
||||
... output_domain="diagnostic_probability"
|
||||
... )
|
||||
>>>
|
||||
>>> # Legal: Evidence Strength → Conviction Probability
|
||||
>>> ba_legal = BridgeAxiom(
|
||||
... axiom_id="BA-LEGAL-001",
|
||||
... name="dna_match_conviction",
|
||||
... rule="DNA match increases conviction probability by 0.95",
|
||||
... coefficient=0.95,
|
||||
... source_doi="10.1016/j.forsciint.2019.12345",
|
||||
... input_domain="forensic_evidence",
|
||||
... output_domain="legal_conclusion"
|
||||
... )
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
@dataclass
|
||||
class BridgeAxiom:
|
||||
"""
|
||||
Bridge axiom for domain translation with provenance.
|
||||
|
||||
Represents a rule that translates data from one domain to another,
|
||||
with complete source tracking for audit-grade provenance.
|
||||
|
||||
Attributes:
|
||||
axiom_id: Unique axiom identifier (e.g., "BA-001")
|
||||
name: Human-readable axiom name
|
||||
rule: Rule description in natural language
|
||||
coefficient: Numeric coefficient for translation
|
||||
source_doi: DOI of source paper/document
|
||||
source_page: Page/table/figure in source
|
||||
source_quote: Direct quote supporting the coefficient
|
||||
confidence: Confidence score (0.0-1.0)
|
||||
input_domain: Input domain (e.g., "ecological")
|
||||
output_domain: Output domain (e.g., "financial")
|
||||
metadata: Additional metadata
|
||||
|
||||
Examples:
|
||||
>>> # Blue Finance
|
||||
>>> ba_finance = BridgeAxiom(
|
||||
... axiom_id="BA-FINANCE-001",
|
||||
... name="biomass_tourism_elasticity",
|
||||
... rule="1% biomass increase → 0.346% tourism revenue increase",
|
||||
... coefficient=0.346,
|
||||
... source_doi="10.1038/s41586-021-03371-z",
|
||||
... input_domain="ecological",
|
||||
... output_domain="financial"
|
||||
... )
|
||||
>>>
|
||||
>>> # Healthcare
|
||||
>>> ba_health = BridgeAxiom(
|
||||
... axiom_id="BA-HEALTH-001",
|
||||
... name="symptom_diagnosis_correlation",
|
||||
... rule="Symptom X increases diagnosis Y probability by 0.75",
|
||||
... coefficient=0.75,
|
||||
... source_doi="10.1001/jama.2020.12345",
|
||||
... input_domain="clinical_symptom",
|
||||
... output_domain="diagnosis"
|
||||
... )
|
||||
>>>
|
||||
>>> # Pharmaceutical
|
||||
>>> ba_pharma = BridgeAxiom(
|
||||
... axiom_id="BA-PHARMA-001",
|
||||
... name="dosage_efficacy_relationship",
|
||||
... rule="10mg increase → 0.15 efficacy improvement",
|
||||
... coefficient=0.15,
|
||||
... source_doi="10.1056/NEJMoa2020123",
|
||||
... input_domain="drug_dosage",
|
||||
... output_domain="clinical_efficacy"
|
||||
... )
|
||||
"""
|
||||
|
||||
axiom_id: str
|
||||
name: str
|
||||
rule: str
|
||||
coefficient: float
|
||||
source_doi: str
|
||||
source_page: str
|
||||
source_quote: Optional[str] = None
|
||||
confidence: float = 1.0
|
||||
input_domain: str = "unknown"
|
||||
output_domain: str = "unknown"
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
input_entity: str,
|
||||
input_value: float,
|
||||
prov_manager: Optional[Any] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Apply bridge axiom to input value with provenance tracking.
|
||||
|
||||
Args:
|
||||
input_entity: Input entity identifier
|
||||
input_value: Input value to transform
|
||||
prov_manager: ProvenanceManager instance (optional)
|
||||
**kwargs: Additional parameters
|
||||
|
||||
Returns:
|
||||
Dictionary with result and provenance information
|
||||
|
||||
Example:
|
||||
>>> result = ba.apply(
|
||||
... input_entity="cabo_pulmo_biomass",
|
||||
... input_value=463,
|
||||
... prov_manager=prov_mgr
|
||||
... )
|
||||
>>> print(result["output_value"])
|
||||
160.098
|
||||
"""
|
||||
# Calculate output value
|
||||
output_value = input_value * self.coefficient
|
||||
|
||||
# Generate output entity ID
|
||||
output_entity = f"{input_entity}_transformed_{self.axiom_id}"
|
||||
|
||||
# Track provenance if manager provided
|
||||
if prov_manager:
|
||||
try:
|
||||
# Track the bridge axiom application
|
||||
prov_manager.track_entity(
|
||||
entity_id=output_entity,
|
||||
source=self.source_doi,
|
||||
entity_type="bridge_axiom_result",
|
||||
activity_id=f"bridge_axiom_application_{self.axiom_id}",
|
||||
source_location=self.source_page,
|
||||
source_quote=self.source_quote,
|
||||
confidence=self.confidence,
|
||||
metadata={
|
||||
"axiom_id": self.axiom_id,
|
||||
"axiom_name": self.name,
|
||||
"rule": self.rule,
|
||||
"coefficient": self.coefficient,
|
||||
"input_entity": input_entity,
|
||||
"input_value": input_value,
|
||||
"output_value": output_value,
|
||||
"input_domain": self.input_domain,
|
||||
"output_domain": self.output_domain,
|
||||
**kwargs
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass # Graceful failure
|
||||
|
||||
return {
|
||||
"axiom_id": self.axiom_id,
|
||||
"axiom_name": self.name,
|
||||
"input_entity": input_entity,
|
||||
"input_value": input_value,
|
||||
"output_entity": output_entity,
|
||||
"output_value": output_value,
|
||||
"coefficient": self.coefficient,
|
||||
"confidence": self.confidence,
|
||||
"source_doi": self.source_doi,
|
||||
"source_page": self.source_page,
|
||||
"input_domain": self.input_domain,
|
||||
"output_domain": self.output_domain
|
||||
}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert bridge axiom to dictionary."""
|
||||
return {
|
||||
"axiom_id": self.axiom_id,
|
||||
"name": self.name,
|
||||
"rule": self.rule,
|
||||
"coefficient": self.coefficient,
|
||||
"source_doi": self.source_doi,
|
||||
"source_page": self.source_page,
|
||||
"source_quote": self.source_quote,
|
||||
"confidence": self.confidence,
|
||||
"input_domain": self.input_domain,
|
||||
"output_domain": self.output_domain,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationChain:
|
||||
"""
|
||||
Multi-layer translation chain with complete provenance.
|
||||
|
||||
Tracks a complete translation from source data through multiple
|
||||
bridge axioms to final output (e.g., L1 → L2 → L3).
|
||||
|
||||
Attributes:
|
||||
chain_id: Unique chain identifier
|
||||
layers: List of layer dictionaries
|
||||
confidence: Overall confidence score
|
||||
metadata: Additional metadata
|
||||
|
||||
Example:
|
||||
>>> chain = TranslationChain(
|
||||
... chain_id="chain_001",
|
||||
... layers=[
|
||||
... {"layer": "L1", "type": "ecological", "value": 463},
|
||||
... {"layer": "L2", "type": "bridge_axiom", "axiom": "BA-001"},
|
||||
... {"layer": "L3", "type": "financial", "value": 29.27}
|
||||
... ]
|
||||
... )
|
||||
"""
|
||||
|
||||
chain_id: str
|
||||
layers: List[Dict[str, Any]] = field(default_factory=list)
|
||||
confidence: float = 1.0
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def add_layer(
|
||||
self,
|
||||
layer_name: str,
|
||||
layer_type: str,
|
||||
value: Any,
|
||||
source: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Add a layer to the translation chain.
|
||||
|
||||
Args:
|
||||
layer_name: Layer name (e.g., "L1", "L2", "L3")
|
||||
layer_type: Layer type (e.g., "ecological", "bridge_axiom", "financial")
|
||||
value: Layer value
|
||||
source: Source document/DOI
|
||||
**kwargs: Additional layer metadata
|
||||
"""
|
||||
layer = {
|
||||
"layer": layer_name,
|
||||
"type": layer_type,
|
||||
"value": value,
|
||||
"source": source,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
**kwargs
|
||||
}
|
||||
self.layers.append(layer)
|
||||
|
||||
def get_layer(self, layer_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get a specific layer by name.
|
||||
|
||||
Args:
|
||||
layer_name: Layer name to retrieve
|
||||
|
||||
Returns:
|
||||
Layer dictionary or None
|
||||
"""
|
||||
for layer in self.layers:
|
||||
if layer.get("layer") == layer_name:
|
||||
return layer
|
||||
return None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert translation chain to dictionary."""
|
||||
return {
|
||||
"chain_id": self.chain_id,
|
||||
"layers": self.layers,
|
||||
"confidence": self.confidence,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
|
||||
def create_translation_chain(
|
||||
input_data: Dict[str, Any],
|
||||
bridge_axioms: List[BridgeAxiom],
|
||||
prov_manager: Optional[Any] = None
|
||||
) -> TranslationChain:
|
||||
"""
|
||||
Create a complete translation chain through multiple bridge axioms.
|
||||
|
||||
Args:
|
||||
input_data: Input data dictionary with 'entity_id' and 'value'
|
||||
bridge_axioms: List of BridgeAxiom objects to apply in sequence
|
||||
prov_manager: ProvenanceManager instance (optional)
|
||||
|
||||
Returns:
|
||||
TranslationChain object with complete provenance
|
||||
|
||||
Example:
|
||||
>>> input_data = {
|
||||
... "entity_id": "cabo_pulmo_biomass",
|
||||
... "value": 463,
|
||||
... "source": "DOI:10.1371/journal.pone.0023601"
|
||||
... }
|
||||
>>> axioms = [ba_001, ba_002]
|
||||
>>> chain = create_translation_chain(input_data, axioms, prov_mgr)
|
||||
"""
|
||||
chain_id = str(uuid.uuid4())
|
||||
chain = TranslationChain(chain_id=chain_id)
|
||||
|
||||
# Add L1 (input layer)
|
||||
chain.add_layer(
|
||||
layer_name="L1",
|
||||
layer_type="input",
|
||||
value=input_data.get("value"),
|
||||
source=input_data.get("source"),
|
||||
entity_id=input_data.get("entity_id")
|
||||
)
|
||||
|
||||
# Apply bridge axioms sequentially
|
||||
current_value = input_data.get("value")
|
||||
current_entity = input_data.get("entity_id")
|
||||
|
||||
for i, axiom in enumerate(bridge_axioms):
|
||||
# Apply axiom
|
||||
result = axiom.apply(
|
||||
input_entity=current_entity,
|
||||
input_value=current_value,
|
||||
prov_manager=prov_manager
|
||||
)
|
||||
|
||||
# Add bridge axiom layer
|
||||
chain.add_layer(
|
||||
layer_name=f"L{i+2}_axiom",
|
||||
layer_type="bridge_axiom",
|
||||
value=axiom.coefficient,
|
||||
source=axiom.source_doi,
|
||||
axiom_id=axiom.axiom_id,
|
||||
axiom_name=axiom.name,
|
||||
rule=axiom.rule
|
||||
)
|
||||
|
||||
# Update for next iteration
|
||||
current_value = result["output_value"]
|
||||
current_entity = result["output_entity"]
|
||||
|
||||
# Update chain confidence (minimum of all confidences)
|
||||
chain.confidence = min(chain.confidence, axiom.confidence)
|
||||
|
||||
# Add final output layer
|
||||
chain.add_layer(
|
||||
layer_name=f"L{len(bridge_axioms)+2}",
|
||||
layer_type="output",
|
||||
value=current_value,
|
||||
entity_id=current_entity
|
||||
)
|
||||
|
||||
return chain
|
||||
|
||||
|
||||
def trace_translation_chain(
|
||||
chain: TranslationChain,
|
||||
prov_manager: Any
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Trace complete provenance for a translation chain.
|
||||
|
||||
Args:
|
||||
chain: TranslationChain object
|
||||
prov_manager: ProvenanceManager instance
|
||||
|
||||
Returns:
|
||||
Dictionary with complete provenance trace
|
||||
|
||||
Example:
|
||||
>>> trace = trace_translation_chain(chain, prov_mgr)
|
||||
>>> print(trace["layers"])
|
||||
"""
|
||||
trace = {
|
||||
"chain_id": chain.chain_id,
|
||||
"layers": [],
|
||||
"confidence": chain.confidence,
|
||||
"provenance": []
|
||||
}
|
||||
|
||||
for layer in chain.layers:
|
||||
layer_trace = {
|
||||
"layer": layer.get("layer"),
|
||||
"type": layer.get("type"),
|
||||
"value": layer.get("value"),
|
||||
"source": layer.get("source")
|
||||
}
|
||||
|
||||
# Get provenance for entities in this layer
|
||||
entity_id = layer.get("entity_id")
|
||||
if entity_id:
|
||||
try:
|
||||
lineage = prov_manager.get_lineage(entity_id)
|
||||
layer_trace["provenance"] = lineage
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
trace["layers"].append(layer_trace)
|
||||
|
||||
return trace
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Integrity Verification Utilities
|
||||
|
||||
This module provides utilities for data integrity verification using
|
||||
SHA-256 checksums, ensuring provenance data has not been tampered with.
|
||||
|
||||
Features:
|
||||
- SHA-256 checksum computation
|
||||
- Checksum verification
|
||||
- Data integrity validation
|
||||
- Tamper detection
|
||||
|
||||
Compliance:
|
||||
- FDA 21 CFR Part 11 (electronic records)
|
||||
- SOX (Sarbanes-Oxley)
|
||||
- HIPAA (healthcare data integrity)
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from typing import Any, Dict, Optional
|
||||
from .schemas import ProvenanceEntry
|
||||
|
||||
|
||||
def compute_checksum(entry: ProvenanceEntry) -> str:
|
||||
"""
|
||||
Compute SHA-256 checksum for a provenance entry.
|
||||
|
||||
Creates a deterministic checksum based on critical provenance fields
|
||||
to detect any tampering or corruption of provenance data.
|
||||
|
||||
Args:
|
||||
entry: ProvenanceEntry to compute checksum for
|
||||
|
||||
Returns:
|
||||
SHA-256 checksum as hexadecimal string
|
||||
|
||||
Example:
|
||||
>>> entry = ProvenanceEntry(
|
||||
... entity_id="entity_123",
|
||||
... entity_type="entity",
|
||||
... activity_id="extraction",
|
||||
... source_document="DOI:10.1371/..."
|
||||
... )
|
||||
>>> checksum = compute_checksum(entry)
|
||||
>>> print(checksum)
|
||||
'a3b2c1d4e5f6...'
|
||||
"""
|
||||
# Concatenate critical fields for checksum
|
||||
data = (
|
||||
f"{entry.entity_id}"
|
||||
f"{entry.entity_type}"
|
||||
f"{entry.activity_id}"
|
||||
f"{entry.source_document}"
|
||||
f"{entry.timestamp}"
|
||||
f"{entry.confidence}"
|
||||
)
|
||||
|
||||
return hashlib.sha256(data.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def verify_checksum(entry: ProvenanceEntry, expected_checksum: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Verify checksum for a provenance entry.
|
||||
|
||||
Computes the current checksum and compares it with the expected checksum
|
||||
to detect tampering or corruption.
|
||||
|
||||
Args:
|
||||
entry: ProvenanceEntry to verify
|
||||
expected_checksum: Expected checksum (uses entry.checksum if None)
|
||||
|
||||
Returns:
|
||||
True if checksum matches, False otherwise
|
||||
|
||||
Example:
|
||||
>>> entry = ProvenanceEntry(...)
|
||||
>>> entry.checksum = compute_checksum(entry)
|
||||
>>> is_valid = verify_checksum(entry)
|
||||
>>> print(is_valid)
|
||||
True
|
||||
"""
|
||||
if expected_checksum is None:
|
||||
expected_checksum = entry.checksum
|
||||
|
||||
if expected_checksum is None:
|
||||
return False
|
||||
|
||||
current_checksum = compute_checksum(entry)
|
||||
return current_checksum == expected_checksum
|
||||
|
||||
|
||||
def compute_data_checksum(data: str) -> str:
|
||||
"""
|
||||
Compute SHA-256 checksum for arbitrary data.
|
||||
|
||||
Args:
|
||||
data: String data to compute checksum for
|
||||
|
||||
Returns:
|
||||
SHA-256 checksum as hexadecimal string
|
||||
|
||||
Example:
|
||||
>>> checksum = compute_data_checksum("some data")
|
||||
>>> print(checksum)
|
||||
'a3b2c1d4e5f6...'
|
||||
"""
|
||||
return hashlib.sha256(data.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def verify_data_checksum(data: str, expected_checksum: str) -> bool:
|
||||
"""
|
||||
Verify checksum for arbitrary data.
|
||||
|
||||
Args:
|
||||
data: String data to verify
|
||||
expected_checksum: Expected checksum
|
||||
|
||||
Returns:
|
||||
True if checksum matches, False otherwise
|
||||
|
||||
Example:
|
||||
>>> data = "some data"
|
||||
>>> checksum = compute_data_checksum(data)
|
||||
>>> is_valid = verify_data_checksum(data, checksum)
|
||||
>>> print(is_valid)
|
||||
True
|
||||
"""
|
||||
current_checksum = compute_data_checksum(data)
|
||||
return current_checksum == expected_checksum
|
||||
|
||||
|
||||
def compute_dict_checksum(data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Compute SHA-256 checksum for dictionary data.
|
||||
|
||||
Sorts keys to ensure deterministic checksum computation.
|
||||
|
||||
Args:
|
||||
data: Dictionary data to compute checksum for
|
||||
|
||||
Returns:
|
||||
SHA-256 checksum as hexadecimal string
|
||||
|
||||
Example:
|
||||
>>> data = {"key1": "value1", "key2": "value2"}
|
||||
>>> checksum = compute_dict_checksum(data)
|
||||
>>> print(checksum)
|
||||
'a3b2c1d4e5f6...'
|
||||
"""
|
||||
# Sort keys for deterministic checksum
|
||||
sorted_items = sorted(data.items())
|
||||
data_str = str(sorted_items)
|
||||
return hashlib.sha256(data_str.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def verify_dict_checksum(data: Dict[str, Any], expected_checksum: str) -> bool:
|
||||
"""
|
||||
Verify checksum for dictionary data.
|
||||
|
||||
Args:
|
||||
data: Dictionary data to verify
|
||||
expected_checksum: Expected checksum
|
||||
|
||||
Returns:
|
||||
True if checksum matches, False otherwise
|
||||
|
||||
Example:
|
||||
>>> data = {"key1": "value1", "key2": "value2"}
|
||||
>>> checksum = compute_dict_checksum(data)
|
||||
>>> is_valid = verify_dict_checksum(data, checksum)
|
||||
>>> print(is_valid)
|
||||
True
|
||||
"""
|
||||
current_checksum = compute_dict_checksum(data)
|
||||
return current_checksum == expected_checksum
|
||||
@@ -0,0 +1,574 @@
|
||||
"""
|
||||
Unified Provenance Manager
|
||||
|
||||
This module provides the central ProvenanceManager class that consolidates
|
||||
provenance tracking from multiple Semantica modules:
|
||||
- kg.ProvenanceTracker (entity/relationship tracking)
|
||||
- split.ProvenanceTracker (chunk tracking)
|
||||
- conflicts.SourceTracker (source tracking)
|
||||
|
||||
The ProvenanceManager provides a unified API for all provenance operations
|
||||
while maintaining backward compatibility with existing tracker interfaces.
|
||||
|
||||
Features:
|
||||
- W3C PROV-O compliant tracking
|
||||
- Entity and relationship tracking
|
||||
- Chunk provenance tracking
|
||||
- Source and property tracking
|
||||
- Complete lineage tracing
|
||||
- Multiple storage backends
|
||||
- Integrity verification
|
||||
- Batch operations
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from .schemas import ProvenanceEntry, SourceReference, PropertySource
|
||||
from .storage import ProvenanceStorage, InMemoryStorage, SQLiteStorage
|
||||
from .integrity import compute_checksum
|
||||
|
||||
|
||||
class ProvenanceManager:
|
||||
"""
|
||||
Unified provenance tracking manager.
|
||||
|
||||
Consolidates and enhances provenance tracking from:
|
||||
- kg.ProvenanceTracker: Entity/relationship tracking with temporal info
|
||||
- split.ProvenanceTracker: Chunk tracking with parent-child relationships
|
||||
- conflicts.SourceTracker: Source tracking with credibility scores
|
||||
|
||||
Example:
|
||||
>>> # Basic usage
|
||||
>>> prov_mgr = ProvenanceManager()
|
||||
>>> prov_mgr.track_entity("entity_1", source="doc_1")
|
||||
>>>
|
||||
>>> # With persistent storage
|
||||
>>> prov_mgr = ProvenanceManager(storage_path="provenance.db")
|
||||
>>>
|
||||
>>> # Trace lineage
|
||||
>>> lineage = prov_mgr.get_lineage("entity_1")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
storage: Optional[ProvenanceStorage] = None,
|
||||
storage_path: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Initialize provenance manager.
|
||||
|
||||
Args:
|
||||
storage: Custom storage backend (optional)
|
||||
storage_path: Path to SQLite database (optional, uses in-memory if None)
|
||||
"""
|
||||
if storage:
|
||||
self.storage = storage
|
||||
elif storage_path:
|
||||
self.storage = SQLiteStorage(storage_path)
|
||||
else:
|
||||
self.storage = InMemoryStorage()
|
||||
|
||||
# === Entity Tracking (from kg.ProvenanceTracker) ===
|
||||
|
||||
def track_entity(
|
||||
self,
|
||||
entity_id: str,
|
||||
source: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> ProvenanceEntry:
|
||||
"""
|
||||
Track entity provenance (kg.ProvenanceTracker compatible).
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
source: Source identifier (document ID, DOI, file path)
|
||||
metadata: Optional metadata dictionary
|
||||
**kwargs: Additional fields (confidence, source_location, etc.)
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry object
|
||||
|
||||
Example:
|
||||
>>> prov_mgr.track_entity(
|
||||
... entity_id="entity_1",
|
||||
... source="DOI:10.1371/journal.pone.0023601",
|
||||
... metadata={"confidence": 0.92}
|
||||
... )
|
||||
"""
|
||||
# Validate entity_id
|
||||
if entity_id is None:
|
||||
raise ValueError("entity_id cannot be None")
|
||||
if not isinstance(entity_id, str):
|
||||
raise TypeError(f"entity_id must be a string, got {type(entity_id).__name__}")
|
||||
|
||||
if not isinstance(entity_id, str):
|
||||
raise TypeError(f"entity_id must be a string, got {type(entity_id).__name__}")
|
||||
|
||||
if not isinstance(entity_id, str):
|
||||
raise TypeError(f"entity_id must be a string, got {type(entity_id).__name__}")
|
||||
|
||||
# Check if entity already exists
|
||||
existing = self.storage.retrieve(entity_id)
|
||||
parent_id = kwargs.get("parent_entity_id")
|
||||
|
||||
# If source is a known entity, link it as parent (unless parent already set)
|
||||
if not parent_id and source and isinstance(source, str):
|
||||
try:
|
||||
# Check if source exists in storage
|
||||
# trace_lineage is cheaper than retrieve for just checking existence? Or retrieve?
|
||||
# retrieve returns the *latest* entry for that ID
|
||||
source_entity = self.storage.retrieve(source)
|
||||
if source_entity:
|
||||
parent_id = source
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If entity exists, preserve history by archiving the old state
|
||||
if existing:
|
||||
# Create a history entry for the previous state
|
||||
# Use timestamp or counter for uniqueness
|
||||
import copy
|
||||
history_entry = copy.deepcopy(existing)
|
||||
history_id = f"{entity_id}:v:{existing.last_updated}"
|
||||
|
||||
# Ensure unique ID if update happens same second
|
||||
if self.storage.retrieve(history_id):
|
||||
history_id = f"{history_id}:{datetime.utcnow().microsecond}"
|
||||
|
||||
history_entry.entity_id = history_id
|
||||
|
||||
# Store the history entry
|
||||
try:
|
||||
self.storage.store(history_entry)
|
||||
# Link new entry to this history entry
|
||||
parent_id = history_id
|
||||
except Exception:
|
||||
pass # If history archiving fails, proceed with update but lose history (graceful degradation)
|
||||
|
||||
entry = ProvenanceEntry(
|
||||
entity_id=entity_id,
|
||||
entity_type=kwargs.get("entity_type", "entity"),
|
||||
activity_id=kwargs.get("activity_id", "entity_tracking"),
|
||||
source_document=source,
|
||||
source_location=kwargs.get("source_location"),
|
||||
source_quote=kwargs.get("source_quote"),
|
||||
confidence=kwargs.get("confidence", 1.0),
|
||||
metadata=metadata or {},
|
||||
first_seen=existing.first_seen if existing else datetime.utcnow().isoformat(),
|
||||
last_updated=datetime.utcnow().isoformat(),
|
||||
parent_entity_id=parent_id # Link to history or explicit parent
|
||||
)
|
||||
|
||||
# Compute checksum for integrity
|
||||
entry.checksum = compute_checksum(entry)
|
||||
|
||||
try:
|
||||
self.storage.store(entry)
|
||||
except Exception:
|
||||
pass # Graceful failure - don't break main functionality
|
||||
|
||||
return entry
|
||||
|
||||
def track_relationship(
|
||||
self,
|
||||
relationship_id: str,
|
||||
source: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> ProvenanceEntry:
|
||||
"""
|
||||
Track relationship provenance (kg.ProvenanceTracker compatible).
|
||||
|
||||
Args:
|
||||
relationship_id: Relationship identifier
|
||||
source: Source identifier
|
||||
metadata: Optional metadata dictionary
|
||||
**kwargs: Additional fields
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry object
|
||||
|
||||
Example:
|
||||
>>> prov_mgr.track_relationship(
|
||||
... relationship_id="rel_1",
|
||||
... source="doc_1",
|
||||
... metadata={"type": "founded"}
|
||||
... )
|
||||
"""
|
||||
entry = ProvenanceEntry(
|
||||
entity_id=relationship_id,
|
||||
entity_type="relationship",
|
||||
activity_id=kwargs.get("activity_id", "relationship_tracking"),
|
||||
source_document=source,
|
||||
source_location=kwargs.get("source_location"),
|
||||
confidence=kwargs.get("confidence", 1.0),
|
||||
metadata=metadata or {},
|
||||
first_seen=datetime.utcnow().isoformat(),
|
||||
last_updated=datetime.utcnow().isoformat()
|
||||
)
|
||||
|
||||
entry.checksum = compute_checksum(entry)
|
||||
|
||||
try:
|
||||
self.storage.store(entry)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return entry
|
||||
|
||||
# === Chunk Tracking (from split.ProvenanceTracker) ===
|
||||
|
||||
def track_chunk(
|
||||
self,
|
||||
chunk_id: str,
|
||||
source_document: str,
|
||||
source_path: Optional[str] = None,
|
||||
start_index: int = 0,
|
||||
end_index: int = 0,
|
||||
parent_chunk_id: Optional[str] = None,
|
||||
**metadata
|
||||
) -> ProvenanceEntry:
|
||||
"""
|
||||
Track chunk provenance (split.ProvenanceTracker compatible).
|
||||
|
||||
Args:
|
||||
chunk_id: Chunk identifier
|
||||
source_document: Source document identifier
|
||||
source_path: Path to source document
|
||||
start_index: Start character index
|
||||
end_index: End character index
|
||||
parent_chunk_id: Parent chunk ID (if chunk was split)
|
||||
**metadata: Additional metadata
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry object
|
||||
|
||||
Example:
|
||||
>>> prov_mgr.track_chunk(
|
||||
... chunk_id="chunk_1",
|
||||
... source_document="doc_1",
|
||||
... source_path="/path/to/doc.pdf",
|
||||
... start_index=0,
|
||||
... end_index=500
|
||||
... )
|
||||
"""
|
||||
entry = ProvenanceEntry(
|
||||
entity_id=chunk_id,
|
||||
entity_type="chunk",
|
||||
activity_id="chunking",
|
||||
source_document=source_document,
|
||||
source_location=source_path,
|
||||
start_index=start_index,
|
||||
end_index=end_index,
|
||||
parent_entity_id=parent_chunk_id,
|
||||
metadata=metadata,
|
||||
timestamp=datetime.utcnow().isoformat()
|
||||
)
|
||||
|
||||
entry.checksum = compute_checksum(entry)
|
||||
|
||||
try:
|
||||
self.storage.store(entry)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return entry
|
||||
|
||||
# === Source Tracking (from conflicts.SourceTracker) ===
|
||||
|
||||
def track_property_source(
|
||||
self,
|
||||
entity_id: str,
|
||||
property_name: str,
|
||||
value: Any,
|
||||
source: SourceReference,
|
||||
**metadata
|
||||
) -> ProvenanceEntry:
|
||||
"""
|
||||
Track property source (conflicts.SourceTracker compatible).
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
property_name: Property name
|
||||
value: Property value
|
||||
source: SourceReference object
|
||||
**metadata: Additional metadata
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry object
|
||||
|
||||
Example:
|
||||
>>> source = SourceReference(
|
||||
... document="DOI:10.1038/...",
|
||||
... page=4,
|
||||
... confidence=0.92
|
||||
... )
|
||||
>>> prov_mgr.track_property_source(
|
||||
... entity_id="entity_1",
|
||||
... property_name="biomass_increase",
|
||||
... value="463%",
|
||||
... source=source
|
||||
... )
|
||||
"""
|
||||
entry = ProvenanceEntry(
|
||||
entity_id=f"{entity_id}_{property_name}",
|
||||
entity_type="property",
|
||||
activity_id="property_tracking",
|
||||
source_document=source.document,
|
||||
source_location=f"page_{source.page}" if source.page else source.section,
|
||||
confidence=source.confidence,
|
||||
credibility=source.metadata.get("credibility"),
|
||||
metadata={
|
||||
"entity_id": entity_id,
|
||||
"property_name": property_name,
|
||||
"value": value,
|
||||
**metadata,
|
||||
**source.metadata
|
||||
},
|
||||
timestamp=datetime.utcnow().isoformat()
|
||||
)
|
||||
|
||||
entry.checksum = compute_checksum(entry)
|
||||
|
||||
try:
|
||||
self.storage.store(entry)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return entry
|
||||
|
||||
# === Batch Operations ===
|
||||
|
||||
def track_entities_batch(
|
||||
self,
|
||||
entities: List[Dict[str, Any]],
|
||||
source: str,
|
||||
**metadata
|
||||
) -> int:
|
||||
"""
|
||||
Track multiple entities in batch.
|
||||
|
||||
Args:
|
||||
entities: List of entity dictionaries with 'id' key
|
||||
source: Source identifier
|
||||
**metadata: Metadata to apply to all entities
|
||||
|
||||
Returns:
|
||||
Number of entities tracked
|
||||
|
||||
Example:
|
||||
>>> entities = [
|
||||
... {"id": "entity_1", "confidence": 0.9},
|
||||
... {"id": "entity_2", "confidence": 0.85}
|
||||
... ]
|
||||
>>> count = prov_mgr.track_entities_batch(entities, "doc_1")
|
||||
"""
|
||||
tracked_count = 0
|
||||
|
||||
for entity in entities:
|
||||
entity_id = entity.get("id") or entity.get("entity_id")
|
||||
if not entity_id:
|
||||
continue
|
||||
|
||||
entity_metadata = {**metadata, **entity.get("metadata", {})}
|
||||
|
||||
try:
|
||||
self.track_entity(entity_id, source, entity_metadata)
|
||||
tracked_count += 1
|
||||
except Exception:
|
||||
pass # Continue with other entities
|
||||
|
||||
return tracked_count
|
||||
|
||||
def track_chunks_batch(
|
||||
self,
|
||||
chunks: List[Dict[str, Any]],
|
||||
source_document: str,
|
||||
source_path: Optional[str] = None,
|
||||
**metadata
|
||||
) -> int:
|
||||
"""
|
||||
Track multiple chunks in batch.
|
||||
|
||||
Args:
|
||||
chunks: List of chunk dictionaries
|
||||
source_document: Source document identifier
|
||||
source_path: Path to source document
|
||||
**metadata: Metadata to apply to all chunks
|
||||
|
||||
Returns:
|
||||
Number of chunks tracked
|
||||
"""
|
||||
tracked_count = 0
|
||||
|
||||
for chunk in chunks:
|
||||
chunk_id = chunk.get("id") or chunk.get("chunk_id")
|
||||
if not chunk_id:
|
||||
continue
|
||||
|
||||
try:
|
||||
self.track_chunk(
|
||||
chunk_id=chunk_id,
|
||||
source_document=source_document,
|
||||
source_path=source_path,
|
||||
start_index=chunk.get("start_index", 0),
|
||||
end_index=chunk.get("end_index", 0),
|
||||
parent_chunk_id=chunk.get("parent_chunk_id"),
|
||||
**{**metadata, **chunk.get("metadata", {})}
|
||||
)
|
||||
tracked_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return tracked_count
|
||||
|
||||
# === Lineage Retrieval ===
|
||||
|
||||
def get_lineage(self, entity_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get complete lineage for an entity.
|
||||
|
||||
Compatible with all existing tracker interfaces.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
Dictionary containing lineage information including metadata
|
||||
|
||||
Example:
|
||||
>>> lineage = prov_mgr.get_lineage("entity_1")
|
||||
>>> print(lineage["source_documents"])
|
||||
['DOI:10.1371/...', 'doc_2']
|
||||
>>> print(lineage["metadata"])
|
||||
{'text': 'Apple Inc.', 'label': 'ORG'}
|
||||
"""
|
||||
lineage_entries = self.storage.trace_lineage(entity_id)
|
||||
|
||||
if not lineage_entries:
|
||||
return {}
|
||||
|
||||
# Aggregate metadata from all lineage entries
|
||||
# Most recent entry's metadata takes precedence
|
||||
aggregated_metadata = {}
|
||||
for entry in lineage_entries:
|
||||
if entry.metadata:
|
||||
meta = entry.metadata
|
||||
if isinstance(meta, str):
|
||||
try:
|
||||
import json
|
||||
meta = json.loads(meta)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
if isinstance(meta, dict):
|
||||
aggregated_metadata.update(meta)
|
||||
|
||||
return {
|
||||
"entity_id": entity_id,
|
||||
"lineage_chain": [entry.to_dict() for entry in lineage_entries],
|
||||
"source_documents": list(set(
|
||||
e.source_document for e in lineage_entries
|
||||
if e.source_document
|
||||
)),
|
||||
"first_seen": min(
|
||||
(e.first_seen for e in lineage_entries if e.first_seen),
|
||||
default=None
|
||||
),
|
||||
"last_updated": max(
|
||||
(e.last_updated for e in lineage_entries if e.last_updated),
|
||||
default=None
|
||||
),
|
||||
"entity_count": len(lineage_entries),
|
||||
"metadata": aggregated_metadata # Add metadata key
|
||||
}
|
||||
|
||||
def trace_lineage(self, entity_id: str) -> List[ProvenanceEntry]:
|
||||
"""
|
||||
Trace complete lineage and return raw entries.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
List of ProvenanceEntry objects
|
||||
"""
|
||||
return self.storage.trace_lineage(entity_id)
|
||||
|
||||
def get_all_sources(self, entity_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all sources for an entity (kg.ProvenanceTracker compatible).
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
List of source dictionaries
|
||||
"""
|
||||
lineage_entries = self.storage.trace_lineage(entity_id)
|
||||
|
||||
sources = []
|
||||
for entry in lineage_entries:
|
||||
if entry.source_document:
|
||||
sources.append({
|
||||
"source": entry.source_document,
|
||||
"location": entry.source_location,
|
||||
"timestamp": entry.timestamp,
|
||||
"confidence": entry.confidence,
|
||||
"metadata": entry.metadata
|
||||
})
|
||||
|
||||
return sources
|
||||
|
||||
def get_provenance(self, entity_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get provenance for entity (kg.ProvenanceTracker compatible).
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
Provenance dictionary or None
|
||||
"""
|
||||
entry = self.storage.retrieve(entity_id)
|
||||
if entry:
|
||||
return entry.to_dict()
|
||||
return None
|
||||
|
||||
# === Utility Methods ===
|
||||
|
||||
def clear(self) -> int:
|
||||
"""
|
||||
Clear all provenance data.
|
||||
|
||||
Returns:
|
||||
Number of entries cleared
|
||||
"""
|
||||
return self.storage.clear()
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get provenance statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with statistics
|
||||
"""
|
||||
all_entries = self.storage.retrieve_all()
|
||||
|
||||
entity_types = {}
|
||||
for entry in all_entries:
|
||||
entity_types[entry.entity_type] = entity_types.get(entry.entity_type, 0) + 1
|
||||
|
||||
return {
|
||||
"total_entries": len(all_entries),
|
||||
"entity_types": entity_types,
|
||||
"unique_sources": len(set(
|
||||
e.source_document for e in all_entries
|
||||
if e.source_document
|
||||
))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
W3C PROV-O Compliant Provenance Schemas
|
||||
|
||||
This module provides dataclasses for provenance tracking that comply with
|
||||
W3C PROV-O (Provenance Ontology) standards while consolidating functionality
|
||||
from existing Semantica provenance trackers.
|
||||
|
||||
Consolidates:
|
||||
- kg.ProvenanceTracker: Entity/relationship tracking with temporal info
|
||||
- split.ProvenanceInfo: Chunk tracking with parent-child relationships
|
||||
- conflicts.SourceReference: Source tracking with credibility scores
|
||||
|
||||
W3C PROV-O Mapping:
|
||||
- ProvenanceEntry.entity_id → prov:Entity
|
||||
- ProvenanceEntry.activity_id → prov:Activity
|
||||
- ProvenanceEntry.agent_id → prov:Agent
|
||||
- ProvenanceEntry.parent_entity_id → prov:wasDerivedFrom
|
||||
- ProvenanceEntry.used_entities → prov:used
|
||||
- ProvenanceEntry.timestamp → prov:generatedAtTime
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProvenanceEntry:
|
||||
"""
|
||||
W3C PROV-O compliant provenance entry.
|
||||
|
||||
This unified schema consolidates provenance tracking from:
|
||||
- kg.ProvenanceTracker (entity/relationship tracking)
|
||||
- split.ProvenanceInfo (chunk tracking)
|
||||
- conflicts.SourceReference (source tracking)
|
||||
|
||||
Attributes:
|
||||
entity_id: Unique identifier for the entity (prov:Entity)
|
||||
entity_type: Type of entity (entity, chunk, relationship, property, etc.)
|
||||
activity_id: Activity that generated this entity (prov:Activity)
|
||||
agent_id: Agent responsible for the activity (prov:Agent)
|
||||
source_document: Source document identifier (DOI, file path, URL)
|
||||
source_location: Location within source (page, figure, char range)
|
||||
source_quote: Direct quote from source (for audit trail)
|
||||
timestamp: When this provenance entry was created (prov:generatedAtTime)
|
||||
first_seen: When entity was first tracked (from kg.ProvenanceTracker)
|
||||
last_updated: When entity was last updated (from kg.ProvenanceTracker)
|
||||
confidence: Confidence score for this provenance entry (0.0-1.0)
|
||||
checksum: SHA-256 checksum for integrity verification
|
||||
parent_entity_id: Parent entity ID (prov:wasDerivedFrom)
|
||||
used_entities: List of entities used to create this entity (prov:used)
|
||||
start_index: Start character index (from split.ProvenanceInfo)
|
||||
end_index: End character index (from split.ProvenanceInfo)
|
||||
credibility: Source credibility score (from conflicts.SourceTracker)
|
||||
metadata: Additional metadata dictionary
|
||||
version: Provenance schema version
|
||||
|
||||
Example:
|
||||
>>> entry = ProvenanceEntry(
|
||||
... entity_id="entity_123",
|
||||
... entity_type="named_entity",
|
||||
... activity_id="ner_extraction",
|
||||
... source_document="DOI:10.1371/journal.pone.0023601",
|
||||
... source_location="Figure 2",
|
||||
... source_quote="Total fish biomass increased by 463%",
|
||||
... confidence=0.92
|
||||
... )
|
||||
"""
|
||||
|
||||
# W3C PROV-O core entities
|
||||
entity_id: str
|
||||
entity_type: str
|
||||
activity_id: str
|
||||
agent_id: str = "semantica"
|
||||
|
||||
# Audit-grade source tracking
|
||||
source_document: str = ""
|
||||
source_location: Optional[str] = None
|
||||
source_quote: Optional[str] = None
|
||||
|
||||
# Temporal tracking (from kg.ProvenanceTracker)
|
||||
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||
first_seen: Optional[str] = None
|
||||
last_updated: Optional[str] = None
|
||||
|
||||
# Quality metrics
|
||||
confidence: float = 1.0
|
||||
checksum: Optional[str] = None
|
||||
|
||||
# Chain of custody (W3C PROV-O)
|
||||
parent_entity_id: Optional[str] = None
|
||||
used_entities: List[str] = field(default_factory=list)
|
||||
|
||||
# Chunk-specific fields (from split.ProvenanceInfo)
|
||||
start_index: Optional[int] = None
|
||||
end_index: Optional[int] = None
|
||||
|
||||
# Source credibility (from conflicts.SourceTracker)
|
||||
credibility: Optional[float] = None
|
||||
|
||||
# Metadata
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
version: str = "1.0"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert provenance entry to dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of provenance entry
|
||||
"""
|
||||
return {
|
||||
"entity_id": self.entity_id,
|
||||
"entity_type": self.entity_type,
|
||||
"activity_id": self.activity_id,
|
||||
"agent_id": self.agent_id,
|
||||
"source_document": self.source_document,
|
||||
"source_location": self.source_location,
|
||||
"source_quote": self.source_quote,
|
||||
"timestamp": self.timestamp,
|
||||
"first_seen": self.first_seen,
|
||||
"last_updated": self.last_updated,
|
||||
"confidence": self.confidence,
|
||||
"checksum": self.checksum,
|
||||
"parent_entity_id": self.parent_entity_id,
|
||||
"used_entities": self.used_entities,
|
||||
"start_index": self.start_index,
|
||||
"end_index": self.end_index,
|
||||
"credibility": self.credibility,
|
||||
"metadata": self.metadata,
|
||||
"version": self.version,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ProvenanceEntry":
|
||||
"""
|
||||
Create provenance entry from dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing provenance data
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry instance
|
||||
"""
|
||||
return cls(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourceReference:
|
||||
"""
|
||||
Source reference for provenance tracking.
|
||||
|
||||
Compatible with conflicts.SourceReference for backward compatibility.
|
||||
|
||||
Attributes:
|
||||
document: Document identifier (DOI, file path, URL)
|
||||
page: Page number within document
|
||||
section: Section identifier within document
|
||||
line: Line number within document
|
||||
timestamp: When this source was accessed/created
|
||||
confidence: Confidence score for this source (0.0-1.0)
|
||||
metadata: Additional metadata dictionary
|
||||
|
||||
Example:
|
||||
>>> source = SourceReference(
|
||||
... document="DOI:10.1038/s41586-021-03371-z",
|
||||
... page=4,
|
||||
... section="Table S4",
|
||||
... confidence=0.92
|
||||
... )
|
||||
"""
|
||||
|
||||
document: str
|
||||
page: Optional[int] = None
|
||||
section: Optional[str] = None
|
||||
line: Optional[int] = None
|
||||
timestamp: Optional[datetime] = None
|
||||
confidence: float = 1.0
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert source reference to dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of source reference
|
||||
"""
|
||||
return {
|
||||
"document": self.document,
|
||||
"page": self.page,
|
||||
"section": self.section,
|
||||
"line": self.line,
|
||||
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
||||
"confidence": self.confidence,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SourceReference":
|
||||
"""
|
||||
Create source reference from dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing source reference data
|
||||
|
||||
Returns:
|
||||
SourceReference instance
|
||||
"""
|
||||
if "timestamp" in data and isinstance(data["timestamp"], str):
|
||||
data["timestamp"] = datetime.fromisoformat(data["timestamp"])
|
||||
return cls(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PropertySource:
|
||||
"""
|
||||
Property source information for conflict tracking.
|
||||
|
||||
Compatible with conflicts.PropertySource for backward compatibility.
|
||||
|
||||
Attributes:
|
||||
property_name: Name of the property
|
||||
value: Value of the property
|
||||
sources: List of source references
|
||||
entity_id: Entity this property belongs to
|
||||
metadata: Additional metadata dictionary
|
||||
|
||||
Example:
|
||||
>>> prop_source = PropertySource(
|
||||
... property_name="biomass_increase",
|
||||
... value="463%",
|
||||
... sources=[source_ref],
|
||||
... entity_id="cabo_pulmo_mpa"
|
||||
... )
|
||||
"""
|
||||
|
||||
property_name: str
|
||||
value: Any
|
||||
sources: List[SourceReference] = field(default_factory=list)
|
||||
entity_id: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert property source to dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of property source
|
||||
"""
|
||||
return {
|
||||
"property_name": self.property_name,
|
||||
"value": self.value,
|
||||
"sources": [s.to_dict() for s in self.sources],
|
||||
"entity_id": self.entity_id,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "PropertySource":
|
||||
"""
|
||||
Create property source from dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing property source data
|
||||
|
||||
Returns:
|
||||
PropertySource instance
|
||||
"""
|
||||
if "sources" in data:
|
||||
data["sources"] = [
|
||||
SourceReference.from_dict(s) if isinstance(s, dict) else s
|
||||
for s in data["sources"]
|
||||
]
|
||||
return cls(**data)
|
||||
@@ -0,0 +1,489 @@
|
||||
"""
|
||||
Provenance Storage Backends
|
||||
|
||||
This module provides storage backends for provenance tracking, including
|
||||
in-memory and persistent SQLite storage with W3C PROV-O compliance.
|
||||
|
||||
Storage Backends:
|
||||
- InMemoryStorage: Fast in-memory storage for development/testing
|
||||
- SQLiteStorage: Persistent SQLite storage for production use
|
||||
|
||||
Features:
|
||||
- W3C PROV-O compliant schema
|
||||
- Lineage tracing with BFS traversal
|
||||
- Efficient entity retrieval
|
||||
- Type-based filtering
|
||||
- Integrity verification support
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any
|
||||
import sqlite3
|
||||
import json
|
||||
from collections import deque
|
||||
|
||||
from .schemas import ProvenanceEntry
|
||||
|
||||
|
||||
class ProvenanceStorage(ABC):
|
||||
"""
|
||||
Abstract storage interface for provenance tracking.
|
||||
|
||||
All storage backends must implement these methods to ensure
|
||||
consistent provenance tracking across different storage types.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def store(self, entry: ProvenanceEntry) -> None:
|
||||
"""
|
||||
Store a provenance entry.
|
||||
|
||||
Args:
|
||||
entry: ProvenanceEntry to store
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def retrieve(self, entity_id: str) -> Optional[ProvenanceEntry]:
|
||||
"""
|
||||
Retrieve a provenance entry by entity ID.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry if found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def retrieve_all(self, entity_type: Optional[str] = None) -> List[ProvenanceEntry]:
|
||||
"""
|
||||
Retrieve all provenance entries, optionally filtered by type.
|
||||
|
||||
Args:
|
||||
entity_type: Optional entity type filter
|
||||
|
||||
Returns:
|
||||
List of ProvenanceEntry objects
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def trace_lineage(self, entity_id: str) -> List[ProvenanceEntry]:
|
||||
"""
|
||||
Trace complete lineage for an entity.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
List of ProvenanceEntry objects in lineage chain
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def clear(self) -> int:
|
||||
"""
|
||||
Clear all provenance data.
|
||||
|
||||
Returns:
|
||||
Number of entries cleared
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InMemoryStorage(ProvenanceStorage):
|
||||
"""
|
||||
Fast in-memory storage for provenance tracking.
|
||||
|
||||
Suitable for:
|
||||
- Development and testing
|
||||
- Short-lived processes
|
||||
- Small to medium datasets
|
||||
- When persistence is not required
|
||||
|
||||
Features:
|
||||
- O(1) entity retrieval
|
||||
- BFS lineage tracing
|
||||
- Type-based filtering
|
||||
- No external dependencies
|
||||
|
||||
Example:
|
||||
>>> storage = InMemoryStorage()
|
||||
>>> storage.store(entry)
|
||||
>>> lineage = storage.trace_lineage("entity_123")
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize in-memory storage."""
|
||||
self._entries: Dict[str, ProvenanceEntry] = {}
|
||||
|
||||
def store(self, entry: ProvenanceEntry) -> None:
|
||||
"""
|
||||
Store a provenance entry in memory.
|
||||
|
||||
Args:
|
||||
entry: ProvenanceEntry to store
|
||||
"""
|
||||
self._entries[entry.entity_id] = entry
|
||||
|
||||
def retrieve(self, entity_id: str) -> Optional[ProvenanceEntry]:
|
||||
"""
|
||||
Retrieve a provenance entry by entity ID.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry if found, None otherwise
|
||||
"""
|
||||
return self._entries.get(entity_id)
|
||||
|
||||
def retrieve_all(self, entity_type: Optional[str] = None) -> List[ProvenanceEntry]:
|
||||
"""
|
||||
Retrieve all provenance entries, optionally filtered by type.
|
||||
|
||||
Args:
|
||||
entity_type: Optional entity type filter
|
||||
|
||||
Returns:
|
||||
List of ProvenanceEntry objects
|
||||
"""
|
||||
if entity_type:
|
||||
return [
|
||||
entry for entry in self._entries.values()
|
||||
if entry.entity_type == entity_type
|
||||
]
|
||||
return list(self._entries.values())
|
||||
|
||||
def trace_lineage(self, entity_id: str) -> List[ProvenanceEntry]:
|
||||
"""
|
||||
Trace complete lineage using BFS traversal.
|
||||
|
||||
Traces both parent entities (wasDerivedFrom) and used entities
|
||||
to build complete provenance chain.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
List of ProvenanceEntry objects in lineage chain
|
||||
"""
|
||||
lineage = []
|
||||
visited = set()
|
||||
queue = deque([entity_id])
|
||||
|
||||
while queue:
|
||||
current_id = queue.popleft()
|
||||
|
||||
if current_id in visited:
|
||||
continue
|
||||
|
||||
visited.add(current_id)
|
||||
entry = self.retrieve(current_id)
|
||||
|
||||
if entry:
|
||||
lineage.append(entry)
|
||||
|
||||
# Add parent entity to queue
|
||||
if entry.parent_entity_id:
|
||||
queue.append(entry.parent_entity_id)
|
||||
|
||||
# Add used entities to queue
|
||||
for used_id in entry.used_entities:
|
||||
if used_id not in visited:
|
||||
queue.append(used_id)
|
||||
|
||||
return lineage
|
||||
|
||||
def clear(self) -> int:
|
||||
"""
|
||||
Clear all provenance data.
|
||||
|
||||
Returns:
|
||||
Number of entries cleared
|
||||
"""
|
||||
count = len(self._entries)
|
||||
self._entries.clear()
|
||||
return count
|
||||
|
||||
|
||||
class SQLiteStorage(ProvenanceStorage):
|
||||
"""
|
||||
Persistent SQLite storage for provenance tracking.
|
||||
|
||||
Suitable for:
|
||||
- Production use
|
||||
- Long-term provenance tracking
|
||||
- Large datasets
|
||||
- Audit trail requirements
|
||||
- Regulatory compliance
|
||||
|
||||
Features:
|
||||
- W3C PROV-O compliant schema
|
||||
- Persistent storage
|
||||
- Efficient indexing
|
||||
- Transaction support
|
||||
- Integrity verification
|
||||
|
||||
Example:
|
||||
>>> storage = SQLiteStorage("provenance.db")
|
||||
>>> storage.store(entry)
|
||||
>>> lineage = storage.trace_lineage("entity_123")
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str = "provenance.db"):
|
||||
"""
|
||||
Initialize SQLite storage.
|
||||
|
||||
Args:
|
||||
db_path: Path to SQLite database file
|
||||
"""
|
||||
self.db_path = db_path
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self) -> None:
|
||||
"""Create tables with W3C PROV-O compliant schema."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS provenance (
|
||||
entity_id TEXT PRIMARY KEY,
|
||||
entity_type TEXT NOT NULL,
|
||||
activity_id TEXT NOT NULL,
|
||||
agent_id TEXT DEFAULT 'semantica',
|
||||
source_document TEXT,
|
||||
source_location TEXT,
|
||||
source_quote TEXT,
|
||||
timestamp TEXT NOT NULL,
|
||||
first_seen TEXT,
|
||||
last_updated TEXT,
|
||||
confidence REAL DEFAULT 1.0,
|
||||
checksum TEXT,
|
||||
parent_entity_id TEXT,
|
||||
used_entities TEXT,
|
||||
start_index INTEGER,
|
||||
end_index INTEGER,
|
||||
credibility REAL,
|
||||
metadata TEXT,
|
||||
version TEXT DEFAULT '1.0'
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for efficient querying
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_type
|
||||
ON provenance(entity_type)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_source_document
|
||||
ON provenance(source_document)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_parent_entity
|
||||
ON provenance(parent_entity_id)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def store(self, entry: ProvenanceEntry) -> None:
|
||||
"""
|
||||
Store a provenance entry in SQLite database.
|
||||
|
||||
Args:
|
||||
entry: ProvenanceEntry to store
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO provenance VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
""", (
|
||||
entry.entity_id,
|
||||
entry.entity_type,
|
||||
entry.activity_id,
|
||||
entry.agent_id,
|
||||
entry.source_document,
|
||||
entry.source_location,
|
||||
entry.source_quote,
|
||||
entry.timestamp,
|
||||
entry.first_seen,
|
||||
entry.last_updated,
|
||||
entry.confidence,
|
||||
entry.checksum,
|
||||
entry.parent_entity_id,
|
||||
json.dumps(entry.used_entities),
|
||||
entry.start_index,
|
||||
entry.end_index,
|
||||
entry.credibility,
|
||||
json.dumps(entry.metadata),
|
||||
entry.version
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def retrieve(self, entity_id: str) -> Optional[ProvenanceEntry]:
|
||||
"""
|
||||
Retrieve a provenance entry by entity ID.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry if found, None otherwise
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT * FROM provenance WHERE entity_id = ?
|
||||
""", (entity_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return self._row_to_entry(row)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def retrieve_all(self, entity_type: Optional[str] = None) -> List[ProvenanceEntry]:
|
||||
"""
|
||||
Retrieve all provenance entries, optionally filtered by type.
|
||||
|
||||
Args:
|
||||
entity_type: Optional entity type filter
|
||||
|
||||
Returns:
|
||||
List of ProvenanceEntry objects
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
if entity_type:
|
||||
cursor.execute("""
|
||||
SELECT * FROM provenance WHERE entity_type = ?
|
||||
""", (entity_type,))
|
||||
else:
|
||||
cursor.execute("SELECT * FROM provenance")
|
||||
|
||||
rows = cursor.fetchall()
|
||||
return [self._row_to_entry(row) for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def trace_lineage(self, entity_id: str) -> List[ProvenanceEntry]:
|
||||
"""
|
||||
Trace complete lineage using BFS traversal.
|
||||
|
||||
Args:
|
||||
entity_id: Entity identifier
|
||||
|
||||
Returns:
|
||||
List of ProvenanceEntry objects in lineage chain
|
||||
"""
|
||||
lineage = []
|
||||
visited = set()
|
||||
queue = deque([entity_id])
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
while queue:
|
||||
current_id = queue.popleft()
|
||||
|
||||
if current_id in visited:
|
||||
continue
|
||||
|
||||
visited.add(current_id)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT * FROM provenance WHERE entity_id = ?
|
||||
""", (current_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
entry = self._row_to_entry(row)
|
||||
lineage.append(entry)
|
||||
|
||||
# Add parent entity to queue
|
||||
if entry.parent_entity_id:
|
||||
queue.append(entry.parent_entity_id)
|
||||
|
||||
# Add used entities to queue
|
||||
for used_id in entry.used_entities:
|
||||
if used_id not in visited:
|
||||
queue.append(used_id)
|
||||
|
||||
return lineage
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def clear(self) -> int:
|
||||
"""
|
||||
Clear all provenance data.
|
||||
|
||||
Returns:
|
||||
Number of entries cleared
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(*) FROM provenance")
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("DELETE FROM provenance")
|
||||
conn.commit()
|
||||
|
||||
return count
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _row_to_entry(self, row: tuple) -> ProvenanceEntry:
|
||||
"""
|
||||
Convert database row to ProvenanceEntry.
|
||||
|
||||
Args:
|
||||
row: Database row tuple
|
||||
|
||||
Returns:
|
||||
ProvenanceEntry object
|
||||
"""
|
||||
return ProvenanceEntry(
|
||||
entity_id=row[0],
|
||||
entity_type=row[1],
|
||||
activity_id=row[2],
|
||||
agent_id=row[3],
|
||||
source_document=row[4] or "",
|
||||
source_location=row[5],
|
||||
source_quote=row[6],
|
||||
timestamp=row[7],
|
||||
first_seen=row[8],
|
||||
last_updated=row[9],
|
||||
confidence=row[10],
|
||||
checksum=row[11],
|
||||
parent_entity_id=row[12],
|
||||
used_entities=json.loads(row[13]) if row[13] else [],
|
||||
start_index=row[14],
|
||||
end_index=row[15],
|
||||
credibility=row[16],
|
||||
metadata=json.loads(row[17]) if row[17] else {},
|
||||
version=row[18]
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Provenance-enabled wrappers for reasoning operations.
|
||||
|
||||
Tracks: premises, conclusions, inference rules, confidence scores
|
||||
|
||||
Usage:
|
||||
from semantica.reasoning.reasoning_provenance import ReasoningEngineWithProvenance
|
||||
|
||||
reasoner = ReasoningEngineWithProvenance(provenance=True)
|
||||
result = reasoner.infer(premises)
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class ReasoningEngineWithProvenance:
|
||||
"""Reasoning engine with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .reasoning_engine import ReasoningEngine
|
||||
|
||||
self.provenance = provenance
|
||||
self._engine = ReasoningEngine(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def infer(self, premises: Any, source: str = None, **kwargs):
|
||||
"""Perform inference with provenance tracking."""
|
||||
result = self._engine.infer(premises, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"inference_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "reasoning_engine",
|
||||
entity_type="inference",
|
||||
metadata={
|
||||
"premises_count": len(premises) if hasattr(premises, '__len__') else 1,
|
||||
"confidence": getattr(result, 'confidence', None)
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._engine, name)
|
||||
|
||||
|
||||
__all__ = ['ReasoningEngineWithProvenance']
|
||||
@@ -93,7 +93,7 @@ class LLMExtraction:
|
||||
**config: Configuration options:
|
||||
- model: Model name (default depends on provider)
|
||||
- api_key: API key (from environment if not provided)
|
||||
- temperature: Temperature for generation
|
||||
- temperature: Temperature for generation (None = use model's default)
|
||||
"""
|
||||
self.logger = get_logger("llm_extraction")
|
||||
self.config = config
|
||||
@@ -104,7 +104,7 @@ class LLMExtraction:
|
||||
|
||||
self.provider_name = provider
|
||||
self.model = config.get("model")
|
||||
self.temperature = config.get("temperature", 0.3)
|
||||
self.temperature = config.get("temperature") # None = use model default
|
||||
|
||||
# Initialize provider using new system
|
||||
try:
|
||||
|
||||
@@ -103,6 +103,12 @@ class BaseProvider:
|
||||
"""Check if provider is available."""
|
||||
return True
|
||||
|
||||
def _add_if_set(self, target: dict, source: dict, *keys: str) -> None:
|
||||
"""Add keys from source to target only if their values are not None."""
|
||||
for key in keys:
|
||||
if key in source and source[key] is not None:
|
||||
target[key] = source[key]
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""Generate text - must be implemented."""
|
||||
raise NotImplementedError
|
||||
@@ -377,26 +383,16 @@ class BaseProvider:
|
||||
if client:
|
||||
# Map generate arguments to client arguments
|
||||
# Instructor standardizes on chat.completions.create for OpenAI/Groq/Anthropic/Gemini
|
||||
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"response_model": schema,
|
||||
"max_retries": max_retries,
|
||||
"temperature": kwargs.get("temperature", 0.1), # Low temp for structured
|
||||
"temperature": kwargs.get("temperature") if kwargs.get("temperature") is not None else 0.1,
|
||||
}
|
||||
|
||||
verbose_mode = kwargs.get("verbose", False)
|
||||
if verbose_mode:
|
||||
import sys
|
||||
print(f" [BaseProvider.generate_typed] Using instructor via {provider_name}. Client: {type(client)}", flush=True, file=sys.stdout)
|
||||
self._add_if_set(create_kwargs, kwargs, "max_tokens", "max_completion_tokens",
|
||||
"top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "logit_bias", "user", "top_k")
|
||||
|
||||
# Pass through other common parameters
|
||||
for param in ["max_tokens", "max_completion_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "logit_bias", "user", "top_k"]:
|
||||
if param in kwargs:
|
||||
create_kwargs[param] = kwargs[param]
|
||||
|
||||
# Add provider-specific params
|
||||
if provider_name == "GroqProvider":
|
||||
create_kwargs["response_format"] = {"type": "json_object"}
|
||||
|
||||
@@ -548,20 +544,10 @@ class OpenAIProvider(BaseProvider):
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": kwargs.get("temperature", 0.3),
|
||||
}
|
||||
|
||||
# Support max_tokens and max_completion_tokens (for o1 models)
|
||||
if "max_completion_tokens" in kwargs:
|
||||
create_kwargs["max_completion_tokens"] = kwargs["max_completion_tokens"]
|
||||
elif "max_tokens" in kwargs:
|
||||
create_kwargs["max_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
# Pass through other common parameters
|
||||
for param in ["top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "logit_bias", "user"]:
|
||||
if param in kwargs:
|
||||
create_kwargs[param] = kwargs[param]
|
||||
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_completion_tokens", "max_tokens",
|
||||
"top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "logit_bias", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
return response.choices[0].message.content
|
||||
|
||||
@@ -574,19 +560,9 @@ class OpenAIProvider(BaseProvider):
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"response_format": {"type": "json_object"},
|
||||
"temperature": kwargs.get("temperature", 0.3),
|
||||
}
|
||||
|
||||
# Support max_tokens and max_completion_tokens
|
||||
if "max_completion_tokens" in kwargs:
|
||||
create_kwargs["max_completion_tokens"] = kwargs["max_completion_tokens"]
|
||||
elif "max_tokens" in kwargs:
|
||||
create_kwargs["max_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
# Pass through other common parameters
|
||||
for param in ["top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "logit_bias", "user"]:
|
||||
if param in kwargs:
|
||||
create_kwargs[param] = kwargs[param]
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_completion_tokens", "max_tokens",
|
||||
"top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "logit_bias", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
try:
|
||||
@@ -647,25 +623,18 @@ class GeminiProvider(BaseProvider):
|
||||
"Gemini client not initialized. Set GEMINI_API_KEY or pass api_key."
|
||||
)
|
||||
|
||||
config = {}
|
||||
self._add_if_set(config, kwargs, "temperature", "top_p", "top_k", "stop_sequences", "candidate_count")
|
||||
if "max_tokens" in kwargs:
|
||||
config["max_output_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
if self._use_new_genai:
|
||||
model = kwargs.get("model", self.model)
|
||||
temperature = kwargs.get("temperature", 0.3)
|
||||
create_kwargs = {"model": model, "contents": prompt, "config": {"temperature": temperature}}
|
||||
if "max_tokens" in kwargs:
|
||||
create_kwargs["config"]["max_output_tokens"] = kwargs["max_tokens"]
|
||||
for p in ["top_p", "top_k", "stop_sequences", "candidate_count"]:
|
||||
if p in kwargs:
|
||||
create_kwargs["config"][p] = kwargs[p]
|
||||
resp = self.client.models.generate_content(**create_kwargs)
|
||||
resp = self.client.models.generate_content(
|
||||
model=kwargs.get("model", self.model), contents=prompt, config=config or None
|
||||
)
|
||||
return self._resp_text(resp)
|
||||
else:
|
||||
generation_config = {"temperature": kwargs.get("temperature", 0.3)}
|
||||
if "max_tokens" in kwargs:
|
||||
generation_config["max_output_tokens"] = kwargs["max_tokens"]
|
||||
for param in ["top_p", "top_k", "stop_sequences", "candidate_count"]:
|
||||
if param in kwargs:
|
||||
generation_config[param] = kwargs[param]
|
||||
response = self.client.generate_content(prompt, generation_config=generation_config)
|
||||
response = self.client.generate_content(prompt, generation_config=config or None)
|
||||
return self._resp_text(response)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> dict:
|
||||
@@ -768,29 +737,11 @@ class GroqProvider(BaseProvider):
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": kwargs.get("temperature", 0.3),
|
||||
}
|
||||
|
||||
# Support max_tokens and max_completion_tokens
|
||||
if "max_completion_tokens" in kwargs:
|
||||
create_kwargs["max_completion_tokens"] = kwargs["max_completion_tokens"]
|
||||
elif "max_tokens" in kwargs:
|
||||
create_kwargs["max_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
# Pass through other common parameters
|
||||
for param in ["top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user"]:
|
||||
if param in kwargs:
|
||||
create_kwargs[param] = kwargs[param]
|
||||
|
||||
verbose_mode = kwargs.get("verbose", False)
|
||||
if verbose_mode:
|
||||
import sys
|
||||
print(f" [GroqProvider.generate] Sending request to Groq API (model: {create_kwargs['model']})...", flush=True, file=sys.stdout)
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_completion_tokens", "max_tokens",
|
||||
"top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
if verbose_mode:
|
||||
import sys
|
||||
print(f" [GroqProvider.generate] Response received from Groq.", flush=True, file=sys.stdout)
|
||||
return response.choices[0].message.content
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> dict:
|
||||
@@ -799,37 +750,17 @@ class GroqProvider(BaseProvider):
|
||||
raise ProcessingError("Groq client not initialized.")
|
||||
|
||||
# Groq requires 'json' in the prompt for json_object mode
|
||||
json_prompt = prompt
|
||||
if "json" not in prompt.lower():
|
||||
json_prompt = f"{prompt}\n\nReturn the response as valid JSON only."
|
||||
json_prompt = prompt if "json" in prompt.lower() else f"{prompt}\n\nReturn the response as valid JSON only."
|
||||
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": json_prompt}],
|
||||
"temperature": kwargs.get("temperature", 0.3),
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
|
||||
# Support max_tokens and max_completion_tokens
|
||||
if "max_completion_tokens" in kwargs:
|
||||
create_kwargs["max_completion_tokens"] = kwargs["max_completion_tokens"]
|
||||
elif "max_tokens" in kwargs:
|
||||
create_kwargs["max_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
# Pass through other common parameters
|
||||
for param in ["top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user"]:
|
||||
if param in kwargs:
|
||||
create_kwargs[param] = kwargs[param]
|
||||
|
||||
verbose_mode = kwargs.get("verbose", False)
|
||||
if verbose_mode:
|
||||
import sys
|
||||
print(f" [GroqProvider.generate_structured] Sending structured request to Groq API (model: {create_kwargs['model']})...", flush=True, file=sys.stdout)
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_completion_tokens", "max_tokens",
|
||||
"top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
if verbose_mode:
|
||||
import sys
|
||||
print(f" [GroqProvider.generate_structured] Structured response received from Groq.", flush=True, file=sys.stdout)
|
||||
try:
|
||||
return self._parse_json(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
@@ -961,6 +892,18 @@ class OllamaProvider(BaseProvider):
|
||||
"""Check if provider is available."""
|
||||
return self.client is not None
|
||||
|
||||
def _build_options(self, kwargs: dict) -> Optional[dict]:
|
||||
"""Build Ollama options dict from kwargs."""
|
||||
options = {}
|
||||
self._add_if_set(options, kwargs, "temperature", "top_p", "top_k", "repeat_penalty", "seed")
|
||||
if "max_tokens" in kwargs:
|
||||
options["num_predict"] = kwargs["max_tokens"]
|
||||
if "num_ctx" in kwargs:
|
||||
options["num_ctx"] = kwargs["num_ctx"]
|
||||
elif "context_window" in kwargs:
|
||||
options["num_ctx"] = kwargs["context_window"]
|
||||
return options or None
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""Generate text from prompt."""
|
||||
if not self.client:
|
||||
@@ -968,25 +911,10 @@ class OllamaProvider(BaseProvider):
|
||||
"Ollama client not initialized. Make sure Ollama is running."
|
||||
)
|
||||
|
||||
options = {"temperature": kwargs.get("temperature", 0.3)}
|
||||
|
||||
if "max_tokens" in kwargs:
|
||||
options["num_predict"] = kwargs["max_tokens"]
|
||||
|
||||
if "num_ctx" in kwargs:
|
||||
options["num_ctx"] = kwargs["num_ctx"]
|
||||
elif "context_window" in kwargs:
|
||||
options["num_ctx"] = kwargs["context_window"]
|
||||
|
||||
# Pass through other common options
|
||||
for param in ["top_p", "top_k", "repeat_penalty", "seed"]:
|
||||
if param in kwargs:
|
||||
options[param] = kwargs[param]
|
||||
|
||||
response = self.client.generate(
|
||||
model=kwargs.get("model", self.model),
|
||||
prompt=prompt,
|
||||
options=options,
|
||||
options=self._build_options(kwargs),
|
||||
)
|
||||
return response.get("response", "")
|
||||
|
||||
@@ -996,26 +924,10 @@ class OllamaProvider(BaseProvider):
|
||||
raise ProcessingError("Ollama client not initialized.")
|
||||
|
||||
json_prompt = f"{prompt}\n\nReturn the response as valid JSON only."
|
||||
|
||||
options = {"temperature": kwargs.get("temperature", 0.3)}
|
||||
|
||||
if "max_tokens" in kwargs:
|
||||
options["num_predict"] = kwargs["max_tokens"]
|
||||
|
||||
if "num_ctx" in kwargs:
|
||||
options["num_ctx"] = kwargs["num_ctx"]
|
||||
elif "context_window" in kwargs:
|
||||
options["num_ctx"] = kwargs["context_window"]
|
||||
|
||||
# Pass through other common options
|
||||
for param in ["top_p", "top_k", "repeat_penalty", "seed"]:
|
||||
if param in kwargs:
|
||||
options[param] = kwargs[param]
|
||||
|
||||
response = self.client.generate(
|
||||
model=kwargs.get("model", self.model),
|
||||
prompt=json_prompt,
|
||||
options=options,
|
||||
options=self._build_options(kwargs),
|
||||
)
|
||||
try:
|
||||
return self._parse_json(response.get("response", "{}"))
|
||||
@@ -1049,26 +961,28 @@ class DeepSeekProvider(BaseProvider):
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
if not self.client:
|
||||
raise ProcessingError("DeepSeek client not initialized. Set DEEPSEEK_API_KEY or pass api_key.")
|
||||
|
||||
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": kwargs.get("temperature", 0.3),
|
||||
}
|
||||
if "max_tokens" in kwargs:
|
||||
create_kwargs["max_tokens"] = kwargs["max_tokens"]
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
return response.choices[0].message.content
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[dict, list]:
|
||||
"""Generate structured output."""
|
||||
if not self.client:
|
||||
raise ProcessingError("DeepSeek client not initialized.")
|
||||
response = self.client.chat.completions.create(
|
||||
model=kwargs.get("model", self.model),
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=kwargs.get("temperature", 0.3),
|
||||
)
|
||||
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
try:
|
||||
return self._parse_json(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
Provenance-enabled wrappers for semantic extraction.
|
||||
|
||||
This module provides provenance tracking for all semantic extraction operations:
|
||||
- Named Entity Recognition (NER)
|
||||
- Relation Extraction
|
||||
- Event Detection
|
||||
- Coreference Resolution
|
||||
- Triplet Extraction
|
||||
|
||||
All classes wrap the original extractors and add optional provenance tracking
|
||||
without modifying existing functionality.
|
||||
|
||||
Usage:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import (
|
||||
NERExtractorWithProvenance,
|
||||
RelationExtractorWithProvenance,
|
||||
EventDetectorWithProvenance
|
||||
)
|
||||
|
||||
# Enable provenance tracking
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
entities = ner.extract("Steve Jobs founded Apple.", source="document.pdf")
|
||||
|
||||
# Provenance is automatically tracked for each extracted entity
|
||||
# Access via: ner._prov_manager.get_lineage(entity_id)
|
||||
|
||||
Features:
|
||||
- Zero breaking changes - works exactly like original classes
|
||||
- Opt-in provenance via provenance=True parameter
|
||||
- Tracks: entity text, labels, confidence, source documents
|
||||
- Complete lineage tracing
|
||||
- Graceful degradation if provenance module unavailable
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
import uuid
|
||||
|
||||
|
||||
class ProvenanceMixin:
|
||||
"""
|
||||
Mixin to add provenance tracking to any extractor class.
|
||||
|
||||
This mixin provides the common provenance infrastructure that can be
|
||||
added to any extraction class without modifying its core functionality.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **kwargs):
|
||||
"""
|
||||
Initialize provenance tracking.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**kwargs: Additional arguments passed to parent class
|
||||
"""
|
||||
self.provenance = provenance
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
# Graceful degradation if provenance module not available
|
||||
self.provenance = False
|
||||
|
||||
def _track_extraction(
|
||||
self,
|
||||
entity_id: str,
|
||||
source: str,
|
||||
entity_type: str,
|
||||
**metadata
|
||||
) -> None:
|
||||
"""
|
||||
Track extraction with provenance.
|
||||
|
||||
Args:
|
||||
entity_id: Unique identifier for extracted entity
|
||||
source: Source document or text
|
||||
entity_type: Type of entity (e.g., 'named_entity', 'relation')
|
||||
**metadata: Additional metadata to track
|
||||
"""
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=entity_id,
|
||||
source=source,
|
||||
entity_type=entity_type,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class NERExtractorWithProvenance(ProvenanceMixin):
|
||||
"""
|
||||
Named Entity Recognition extractor with provenance tracking.
|
||||
|
||||
Wraps the original NERExtractor and adds optional provenance tracking
|
||||
for all extracted entities.
|
||||
|
||||
Example:
|
||||
>>> ner = NERExtractorWithProvenance(provenance=True)
|
||||
>>> entities = ner.extract("Steve Jobs founded Apple.", source="doc1.pdf")
|
||||
>>> # Each entity is tracked with source, confidence, and metadata
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize NER extractor with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original NERExtractor
|
||||
"""
|
||||
from .ner_extractor import NERExtractor
|
||||
|
||||
ProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._extractor = NERExtractor(**config)
|
||||
|
||||
def extract(self, text: str, source: Optional[str] = None, **kwargs):
|
||||
"""
|
||||
Extract named entities with provenance tracking.
|
||||
|
||||
Args:
|
||||
text: Input text to extract entities from
|
||||
source: Source document identifier (for provenance)
|
||||
**kwargs: Additional arguments for extraction
|
||||
|
||||
Returns:
|
||||
List of extracted entities (same as original NERExtractor)
|
||||
"""
|
||||
entities = self._extractor.extract(text, **kwargs)
|
||||
|
||||
if self.provenance:
|
||||
for entity in entities:
|
||||
entity_id = getattr(entity, 'id', None)
|
||||
if not entity_id:
|
||||
entity_id = f"entity_{uuid.uuid4().hex[:8]}"
|
||||
try:
|
||||
entity.id = entity_id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
self._track_extraction(
|
||||
entity_id=entity_id,
|
||||
source=source or text[:100],
|
||||
entity_type="named_entity",
|
||||
text=entity.text,
|
||||
label=entity.label,
|
||||
confidence=getattr(entity, 'confidence', 1.0),
|
||||
start=entity.start,
|
||||
end=entity.end
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped extractor."""
|
||||
return getattr(self._extractor, name)
|
||||
|
||||
|
||||
class RelationExtractorWithProvenance(ProvenanceMixin):
|
||||
"""
|
||||
Relation extractor with provenance tracking.
|
||||
|
||||
Wraps the original RelationExtractor and tracks all extracted relations.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize relation extractor with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original RelationExtractor
|
||||
"""
|
||||
from .relation_extractor import RelationExtractor
|
||||
|
||||
ProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._extractor = RelationExtractor(**config)
|
||||
|
||||
def extract(self, text: str, source: Optional[str] = None, **kwargs):
|
||||
"""
|
||||
Extract relations with provenance tracking.
|
||||
|
||||
Args:
|
||||
text: Input text to extract relations from
|
||||
source: Source document identifier (for provenance)
|
||||
**kwargs: Additional arguments for extraction
|
||||
|
||||
Returns:
|
||||
List of extracted relations
|
||||
"""
|
||||
relations = self._extractor.extract(text, **kwargs)
|
||||
|
||||
if self.provenance:
|
||||
for relation in relations:
|
||||
relation_id = getattr(relation, 'id', None)
|
||||
if not relation_id:
|
||||
relation_id = f"rel_{uuid.uuid4().hex[:8]}"
|
||||
try:
|
||||
relation.id = relation_id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
self._track_extraction(
|
||||
entity_id=relation_id,
|
||||
source=source or text[:100],
|
||||
entity_type="relation",
|
||||
subject=relation.subject,
|
||||
predicate=relation.predicate,
|
||||
object=relation.object,
|
||||
confidence=getattr(relation, 'confidence', 1.0)
|
||||
)
|
||||
|
||||
return relations
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped extractor."""
|
||||
return getattr(self._extractor, name)
|
||||
|
||||
|
||||
class EventDetectorWithProvenance(ProvenanceMixin):
|
||||
"""
|
||||
Event detector with provenance tracking.
|
||||
|
||||
Wraps the original EventDetector and tracks all detected events.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize event detector with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original EventDetector
|
||||
"""
|
||||
from .event_detector import EventDetector
|
||||
|
||||
ProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._detector = EventDetector(**config)
|
||||
|
||||
def detect(self, text: str, source: Optional[str] = None, **kwargs):
|
||||
"""
|
||||
Detect events with provenance tracking.
|
||||
|
||||
Args:
|
||||
text: Input text to detect events from
|
||||
source: Source document identifier (for provenance)
|
||||
**kwargs: Additional arguments for detection
|
||||
|
||||
Returns:
|
||||
List of detected events
|
||||
"""
|
||||
events = self._detector.detect(text, **kwargs)
|
||||
|
||||
if self.provenance:
|
||||
for event in events:
|
||||
event_id = getattr(event, 'id', None)
|
||||
if not event_id:
|
||||
event_id = f"event_{uuid.uuid4().hex[:8]}"
|
||||
try:
|
||||
event.id = event_id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
self._track_extraction(
|
||||
entity_id=event_id,
|
||||
source=source or text[:100],
|
||||
entity_type="event",
|
||||
event_type=event.type,
|
||||
trigger=event.trigger,
|
||||
confidence=getattr(event, 'confidence', 1.0)
|
||||
)
|
||||
|
||||
return events
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped detector."""
|
||||
return getattr(self._detector, name)
|
||||
|
||||
|
||||
class CoreferenceResolverWithProvenance(ProvenanceMixin):
|
||||
"""
|
||||
Coreference resolver with provenance tracking.
|
||||
|
||||
Wraps the original CoreferenceResolver and tracks coreference chains.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize coreference resolver with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original CoreferenceResolver
|
||||
"""
|
||||
from .coreference_resolver import CoreferenceResolver
|
||||
|
||||
ProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._resolver = CoreferenceResolver(**config)
|
||||
|
||||
def resolve(self, text: str, source: Optional[str] = None, **kwargs):
|
||||
"""
|
||||
Resolve coreferences with provenance tracking.
|
||||
|
||||
Args:
|
||||
text: Input text to resolve coreferences
|
||||
source: Source document identifier (for provenance)
|
||||
**kwargs: Additional arguments for resolution
|
||||
|
||||
Returns:
|
||||
Coreference chains
|
||||
"""
|
||||
chains = self._resolver.resolve(text, **kwargs)
|
||||
|
||||
if self.provenance:
|
||||
for chain in chains:
|
||||
chain_id = getattr(chain, 'id', None)
|
||||
if not chain_id:
|
||||
chain_id = f"coref_{uuid.uuid4().hex[:8]}"
|
||||
try:
|
||||
chain.id = chain_id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
self._track_extraction(
|
||||
entity_id=chain_id,
|
||||
source=source or text[:100],
|
||||
entity_type="coreference_chain",
|
||||
mentions=len(chain.mentions) if hasattr(chain, 'mentions') else 0
|
||||
)
|
||||
|
||||
return chains
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped resolver."""
|
||||
return getattr(self._resolver, name)
|
||||
|
||||
|
||||
class TripletExtractorWithProvenance(ProvenanceMixin):
|
||||
"""
|
||||
Triplet extractor with provenance tracking.
|
||||
|
||||
Wraps the original TripletExtractor and tracks all extracted triplets.
|
||||
"""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
"""
|
||||
Initialize triplet extractor with optional provenance.
|
||||
|
||||
Args:
|
||||
provenance: Enable provenance tracking (default: False)
|
||||
**config: Configuration passed to original TripletExtractor
|
||||
"""
|
||||
from .triplet_extractor import TripletExtractor
|
||||
|
||||
ProvenanceMixin.__init__(self, provenance=provenance)
|
||||
self._extractor = TripletExtractor(**config)
|
||||
|
||||
def extract(self, text: str, source: Optional[str] = None, **kwargs):
|
||||
"""
|
||||
Extract triplets with provenance tracking.
|
||||
|
||||
Args:
|
||||
text: Input text to extract triplets from
|
||||
source: Source document identifier (for provenance)
|
||||
**kwargs: Additional arguments for extraction
|
||||
|
||||
Returns:
|
||||
List of extracted triplets
|
||||
"""
|
||||
triplets = self._extractor.extract(text, **kwargs)
|
||||
|
||||
if self.provenance:
|
||||
for triplet in triplets:
|
||||
triplet_id = getattr(triplet, 'id', None)
|
||||
if not triplet_id:
|
||||
triplet_id = f"triplet_{uuid.uuid4().hex[:8]}"
|
||||
try:
|
||||
triplet.id = triplet_id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
self._track_extraction(
|
||||
entity_id=triplet_id,
|
||||
source=source or text[:100],
|
||||
entity_type="triplet",
|
||||
subject=triplet.subject,
|
||||
predicate=triplet.predicate,
|
||||
object=triplet.object,
|
||||
confidence=getattr(triplet, 'confidence', 1.0)
|
||||
)
|
||||
|
||||
return triplets
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate other methods to wrapped extractor."""
|
||||
return getattr(self._extractor, name)
|
||||
|
||||
|
||||
# Convenience exports
|
||||
__all__ = [
|
||||
'NERExtractorWithProvenance',
|
||||
'RelationExtractorWithProvenance',
|
||||
'EventDetectorWithProvenance',
|
||||
'CoreferenceResolverWithProvenance',
|
||||
'TripletExtractorWithProvenance',
|
||||
'ProvenanceMixin',
|
||||
]
|
||||
@@ -1,9 +1,17 @@
|
||||
"""
|
||||
Provenance Tracker Module
|
||||
Provenance Tracker Module (Enhanced with Unified Backend)
|
||||
|
||||
This module provides comprehensive source tracking for document chunks,
|
||||
maintaining data lineage and traceability throughout the chunking process.
|
||||
|
||||
IMPORTANT: This module now uses the unified semantica.provenance.ProvenanceManager
|
||||
backend for enhanced W3C PROV-O compliance and audit-grade tracking. All existing
|
||||
APIs remain 100% backward compatible.
|
||||
|
||||
For new code, consider using the unified API:
|
||||
>>> from semantica.provenance import ProvenanceManager
|
||||
>>> prov_mgr = ProvenanceManager()
|
||||
|
||||
Key Features:
|
||||
- Chunk source tracking
|
||||
- Document lineage management
|
||||
@@ -11,9 +19,11 @@ Key Features:
|
||||
- Chunk linking and relationships
|
||||
- Provenance export
|
||||
- Version tracking support
|
||||
- W3C PROV-O compliance (when using unified backend)
|
||||
- Audit-grade integrity verification
|
||||
|
||||
Main Classes:
|
||||
- ProvenanceTracker: Main provenance tracking coordinator
|
||||
- ProvenanceTracker: Main provenance tracking coordinator (backward compatible wrapper)
|
||||
- ProvenanceInfo: Provenance information representation dataclass
|
||||
|
||||
Example Usage:
|
||||
@@ -36,6 +46,13 @@ from ..utils.logging import get_logger
|
||||
from ..utils.progress_tracker import get_progress_tracker
|
||||
from .semantic_chunker import Chunk
|
||||
|
||||
# Import unified provenance manager
|
||||
try:
|
||||
from ..provenance import ProvenanceManager as UnifiedProvenanceManager
|
||||
UNIFIED_AVAILABLE = True
|
||||
except ImportError:
|
||||
UNIFIED_AVAILABLE = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProvenanceInfo:
|
||||
@@ -53,7 +70,12 @@ class ProvenanceInfo:
|
||||
|
||||
|
||||
class ProvenanceTracker:
|
||||
"""Provenance tracker for chunk source tracking."""
|
||||
"""
|
||||
Provenance tracker for chunk source tracking (Enhanced with Unified Backend).
|
||||
|
||||
This class now wraps semantica.provenance.ProvenanceManager for enhanced
|
||||
W3C PROV-O compliance while maintaining 100% API compatibility.
|
||||
"""
|
||||
|
||||
def __init__(self, **config):
|
||||
"""
|
||||
@@ -63,6 +85,7 @@ class ProvenanceTracker:
|
||||
**config: Configuration options:
|
||||
- store_metadata: Store chunk metadata (default: True)
|
||||
- track_versions: Track version history (default: False)
|
||||
- storage_path: Path to SQLite database (optional)
|
||||
"""
|
||||
self.logger = get_logger("provenance_tracker")
|
||||
self.config = config
|
||||
@@ -74,9 +97,22 @@ class ProvenanceTracker:
|
||||
self.store_metadata = config.get("store_metadata", True)
|
||||
self.track_versions = config.get("track_versions", False)
|
||||
|
||||
# In-memory store (could be replaced with database)
|
||||
self._provenance_store: Dict[str, ProvenanceInfo] = {}
|
||||
self._chunk_registry: Dict[str, str] = {} # chunk_id -> provenance_id
|
||||
# Determine whether to use unified backend
|
||||
use_unified = config.get("use_unified", True) and UNIFIED_AVAILABLE
|
||||
|
||||
if use_unified:
|
||||
# Use unified provenance manager
|
||||
storage_path = config.get("storage_path")
|
||||
self._unified_manager = UnifiedProvenanceManager(storage_path=storage_path)
|
||||
self._use_unified = True
|
||||
self.logger.debug("Chunk provenance tracker initialized with unified backend")
|
||||
else:
|
||||
# Fallback to legacy in-memory storage
|
||||
self._unified_manager = None
|
||||
self._use_unified = False
|
||||
self._provenance_store: Dict[str, ProvenanceInfo] = {}
|
||||
self._chunk_registry: Dict[str, str] = {} # chunk_id -> provenance_id
|
||||
self.logger.debug("Chunk provenance tracker initialized with legacy backend")
|
||||
|
||||
def track_chunk(
|
||||
self,
|
||||
@@ -107,6 +143,43 @@ class ProvenanceTracker:
|
||||
except AttributeError:
|
||||
pass # Chunk might be immutable
|
||||
|
||||
if self._use_unified:
|
||||
# Delegate to unified manager
|
||||
try:
|
||||
chunk_metadata = {**chunk.metadata, **metadata, "chunk_size": len(chunk.text)} if self.store_metadata else metadata
|
||||
self._unified_manager.track_chunk(
|
||||
chunk_id=chunk_id,
|
||||
source_document=source_document,
|
||||
source_path=source_path,
|
||||
start_index=chunk.start_index,
|
||||
end_index=chunk.end_index,
|
||||
parent_chunk_id=parent_chunk_id,
|
||||
**chunk_metadata
|
||||
)
|
||||
return chunk_id # Return chunk_id for compatibility
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Unified tracking failed, using fallback: {e}")
|
||||
return self._track_chunk_legacy(chunk, source_document, source_path, parent_chunk_id, **metadata)
|
||||
else:
|
||||
return self._track_chunk_legacy(chunk, source_document, source_path, parent_chunk_id, **metadata)
|
||||
|
||||
def _track_chunk_legacy(
|
||||
self,
|
||||
chunk: Chunk,
|
||||
source_document: str,
|
||||
source_path: Optional[str] = None,
|
||||
parent_chunk_id: Optional[str] = None,
|
||||
**metadata,
|
||||
) -> str:
|
||||
"""Legacy chunk tracking implementation."""
|
||||
chunk_id = getattr(chunk, "id", None)
|
||||
if not chunk_id:
|
||||
chunk_id = str(uuid4())
|
||||
try:
|
||||
chunk.id = chunk_id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
provenance_id = str(uuid4())
|
||||
|
||||
provenance_info = ProvenanceInfo(
|
||||
@@ -214,6 +287,31 @@ class ProvenanceTracker:
|
||||
Returns:
|
||||
ProvenanceInfo: Provenance information or None
|
||||
"""
|
||||
if self._use_unified:
|
||||
try:
|
||||
prov = self._unified_manager.get_provenance(chunk_id)
|
||||
if prov:
|
||||
# Convert to ProvenanceInfo for backward compatibility
|
||||
return ProvenanceInfo(
|
||||
chunk_id=chunk_id,
|
||||
source_document=prov.get("source_document", ""),
|
||||
source_path=prov.get("source_location"),
|
||||
start_index=prov.get("start_index", 0),
|
||||
end_index=prov.get("end_index", 0),
|
||||
parent_chunk_id=prov.get("parent_entity_id"),
|
||||
metadata=prov.get("metadata", {}),
|
||||
version=prov.get("version", "1.0"),
|
||||
timestamp=prov.get("timestamp")
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Unified retrieval failed, using fallback: {e}")
|
||||
return self._get_provenance_legacy(chunk_id)
|
||||
else:
|
||||
return self._get_provenance_legacy(chunk_id)
|
||||
|
||||
def _get_provenance_legacy(self, chunk_id: str) -> Optional[ProvenanceInfo]:
|
||||
"""Legacy get provenance implementation."""
|
||||
provenance_id = self._chunk_registry.get(chunk_id)
|
||||
if provenance_id:
|
||||
return self._provenance_store.get(provenance_id)
|
||||
@@ -245,11 +343,38 @@ class ProvenanceTracker:
|
||||
Returns:
|
||||
list: Lineage chain (oldest to newest)
|
||||
"""
|
||||
if self._use_unified:
|
||||
try:
|
||||
lineage_entries = self._unified_manager.trace_lineage(chunk_id)
|
||||
# Convert to ProvenanceInfo list for backward compatibility
|
||||
return [
|
||||
ProvenanceInfo(
|
||||
chunk_id=entry.entity_id,
|
||||
source_document=entry.source_document,
|
||||
source_path=entry.source_location,
|
||||
start_index=entry.start_index or 0,
|
||||
end_index=entry.end_index or 0,
|
||||
parent_chunk_id=entry.parent_entity_id,
|
||||
metadata=entry.metadata,
|
||||
version=entry.version,
|
||||
timestamp=entry.timestamp
|
||||
)
|
||||
for entry in lineage_entries
|
||||
if entry.entity_type == "chunk"
|
||||
]
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Unified lineage retrieval failed, using fallback: {e}")
|
||||
return self._get_chunk_lineage_legacy(chunk_id)
|
||||
else:
|
||||
return self._get_chunk_lineage_legacy(chunk_id)
|
||||
|
||||
def _get_chunk_lineage_legacy(self, chunk_id: str) -> List[ProvenanceInfo]:
|
||||
"""Legacy get chunk lineage implementation."""
|
||||
lineage = []
|
||||
current_chunk_id = chunk_id
|
||||
|
||||
while current_chunk_id:
|
||||
provenance = self.get_provenance(current_chunk_id)
|
||||
provenance = self._get_provenance_legacy(current_chunk_id)
|
||||
if not provenance:
|
||||
break
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ class JenaStore:
|
||||
)
|
||||
|
||||
try:
|
||||
if not self.graph:
|
||||
if self.graph is None:
|
||||
self.progress_tracker.stop_tracking(
|
||||
tracking_id, status="failed", message="Graph not initialized"
|
||||
)
|
||||
@@ -175,14 +175,14 @@ class JenaStore:
|
||||
return self.add_triplets([triplet], **options)
|
||||
|
||||
def get_triplets(
|
||||
self,
|
||||
subject: Optional[str] = None,
|
||||
predicate: Optional[str] = None,
|
||||
object: Optional[str] = None,
|
||||
**options,
|
||||
self,
|
||||
subject: Optional[str] = None,
|
||||
predicate: Optional[str] = None,
|
||||
object: Optional[str] = None,
|
||||
**options,
|
||||
) -> List[Triplet]:
|
||||
"""Get triplets matching criteria."""
|
||||
if not self.graph:
|
||||
if self.graph is None:
|
||||
return []
|
||||
|
||||
try:
|
||||
@@ -218,7 +218,7 @@ class JenaStore:
|
||||
|
||||
def delete_triplet(self, triplet: Triplet, **options) -> Dict[str, Any]:
|
||||
"""Delete triplet."""
|
||||
if not self.graph:
|
||||
if self.graph is None:
|
||||
raise ProcessingError("Graph not initialized")
|
||||
|
||||
try:
|
||||
@@ -273,7 +273,7 @@ class JenaStore:
|
||||
Returns:
|
||||
Query results
|
||||
"""
|
||||
if not self.graph:
|
||||
if self.graph is None:
|
||||
raise ProcessingError("Graph not initialized")
|
||||
|
||||
try:
|
||||
@@ -319,7 +319,7 @@ class JenaStore:
|
||||
Returns:
|
||||
Serialized RDF string
|
||||
"""
|
||||
if not self.graph:
|
||||
if self.graph is None:
|
||||
return ""
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for triplet storage.
|
||||
|
||||
Tracks: triplets stored
|
||||
|
||||
Usage:
|
||||
from semantica.triplet_store.triplet_store_provenance import TripletStoreWithProvenance
|
||||
|
||||
store = TripletStoreWithProvenance(provenance=True)
|
||||
store.add_triplet(subject, predicate, object, source="kg.json")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class TripletStoreWithProvenance:
|
||||
"""Triplet store with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .triplet_store import TripletStore
|
||||
|
||||
self.provenance = provenance
|
||||
self._store = TripletStore(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def add_triplet(self, subject: Any, predicate: Any, obj: Any, source: str = None, **kwargs):
|
||||
"""Add triplet with provenance tracking."""
|
||||
result = self._store.add_triplet(subject, predicate, obj, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"triplet_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "triplet_store",
|
||||
entity_type="triplet",
|
||||
metadata={
|
||||
"subject": str(subject),
|
||||
"predicate": str(predicate),
|
||||
"object": str(obj)
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._store, name)
|
||||
|
||||
|
||||
__all__ = ['TripletStoreWithProvenance']
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for vector storage.
|
||||
|
||||
Tracks: vectors stored, dimensions
|
||||
|
||||
Usage:
|
||||
from semantica.vector_store.vector_store_provenance import VectorStoreWithProvenance
|
||||
|
||||
store = VectorStoreWithProvenance(provenance=True)
|
||||
store.add_vectors(vectors, source="embeddings.npy")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import List, Any
|
||||
import uuid
|
||||
|
||||
|
||||
class VectorStoreWithProvenance:
|
||||
"""Vector store with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .vector_store import VectorStore
|
||||
|
||||
self.provenance = provenance
|
||||
self._store = VectorStore(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def add_vectors(self, vectors: List[Any], source: str = None, **kwargs):
|
||||
"""Add vectors with provenance tracking."""
|
||||
result = self._store.add_vectors(vectors, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"vectors_{uuid.uuid4().hex[:8]}",
|
||||
source=source or "vector_store",
|
||||
entity_type="vector_collection",
|
||||
metadata={
|
||||
"count": len(vectors),
|
||||
"dimensions": len(vectors[0]) if vectors else 0
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._store, name)
|
||||
|
||||
|
||||
__all__ = ['VectorStoreWithProvenance']
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Provenance-enabled wrapper for visualization.
|
||||
|
||||
Usage:
|
||||
from semantica.visualization.visualization_provenance import VisualizerWithProvenance
|
||||
|
||||
viz = VisualizerWithProvenance(provenance=True)
|
||||
viz.visualize(data, output="graph.png")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
class VisualizerWithProvenance:
|
||||
"""Visualizer with provenance tracking."""
|
||||
|
||||
def __init__(self, provenance: bool = False, **config):
|
||||
from .visualizer import Visualizer
|
||||
|
||||
self.provenance = provenance
|
||||
self._visualizer = Visualizer(**config)
|
||||
self._prov_manager = None
|
||||
|
||||
if provenance:
|
||||
try:
|
||||
from semantica.provenance import ProvenanceManager
|
||||
self._prov_manager = ProvenanceManager()
|
||||
except ImportError:
|
||||
self.provenance = False
|
||||
|
||||
def visualize(self, data: Any, output: str = None, **kwargs):
|
||||
"""Visualize data with provenance tracking."""
|
||||
result = self._visualizer.visualize(data, output=output, **kwargs)
|
||||
|
||||
if self.provenance and self._prov_manager:
|
||||
self._prov_manager.track_entity(
|
||||
entity_id=f"viz_{uuid.uuid4().hex[:8]}",
|
||||
source="visualization",
|
||||
entity_type="visualization",
|
||||
metadata={"output": output, "type": kwargs.get('type', 'unknown')}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._visualizer, name)
|
||||
|
||||
|
||||
__all__ = ['VisualizerWithProvenance']
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Provenance Module Tests
|
||||
|
||||
Test suite for the unified provenance tracking module.
|
||||
|
||||
Test Coverage:
|
||||
- W3C PROV-O schema compliance
|
||||
- Storage backends (InMemory, SQLite)
|
||||
- ProvenanceManager functionality
|
||||
- Integrity verification
|
||||
- Bridge axiom tracking
|
||||
- Backward compatibility
|
||||
- Performance benchmarks
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
"""
|
||||
@@ -0,0 +1,499 @@
|
||||
"""
|
||||
Comprehensive Module Integration Tests
|
||||
|
||||
Tests that provenance tracking is integrated and working correctly across
|
||||
ALL Semantica modules.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
|
||||
class TestSemanticExtractModule:
|
||||
"""Test semantic_extract module provenance integration."""
|
||||
|
||||
def test_semantic_extract_imports(self):
|
||||
"""Test that semantic_extract module can be imported."""
|
||||
try:
|
||||
from semantica import semantic_extract
|
||||
assert semantic_extract is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"semantic_extract module not available: {e}")
|
||||
|
||||
def test_ner_extractor_provenance_ready(self):
|
||||
"""Test NER extractor is ready for provenance tracking."""
|
||||
try:
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
|
||||
# Should be able to instantiate
|
||||
extractor = NERExtractor()
|
||||
assert extractor is not None
|
||||
|
||||
# Check if provenance parameter exists (future integration)
|
||||
import inspect
|
||||
sig = inspect.signature(NERExtractor.__init__)
|
||||
# Note: provenance parameter will be added in future phases
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_relation_extractor_provenance_ready(self):
|
||||
"""Test relation extractor is ready for provenance tracking."""
|
||||
try:
|
||||
from semantica.semantic_extract import RelationExtractor
|
||||
|
||||
extractor = RelationExtractor()
|
||||
assert extractor is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("RelationExtractor not available")
|
||||
|
||||
|
||||
class TestKGModule:
|
||||
"""Test kg module provenance integration."""
|
||||
|
||||
def test_kg_provenance_tracker_available(self):
|
||||
"""Test kg.ProvenanceTracker is available and working."""
|
||||
from semantica.kg import ProvenanceTracker
|
||||
|
||||
tracker = ProvenanceTracker()
|
||||
assert tracker is not None
|
||||
|
||||
# Test basic functionality
|
||||
tracker.track_entity("test_entity", source="test_source")
|
||||
lineage = tracker.get_lineage("test_entity")
|
||||
|
||||
assert lineage is not None
|
||||
assert "sources" in lineage
|
||||
|
||||
def test_kg_uses_unified_backend(self):
|
||||
"""Test that kg module uses unified backend."""
|
||||
from semantica.kg import ProvenanceTracker
|
||||
|
||||
tracker = ProvenanceTracker()
|
||||
|
||||
# Check if using unified backend
|
||||
assert hasattr(tracker, '_use_unified')
|
||||
assert hasattr(tracker, '_unified_manager')
|
||||
|
||||
def test_kg_graph_builder_ready(self):
|
||||
"""Test GraphBuilder is ready for provenance."""
|
||||
try:
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
builder = GraphBuilder()
|
||||
assert builder is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("GraphBuilder not available")
|
||||
|
||||
|
||||
class TestSplitModule:
|
||||
"""Test split module provenance integration."""
|
||||
|
||||
def test_split_provenance_tracker_available(self):
|
||||
"""Test split.ProvenanceTracker is available and working."""
|
||||
from semantica.split import ProvenanceTracker
|
||||
from semantica.split.semantic_chunker import Chunk
|
||||
|
||||
tracker = ProvenanceTracker()
|
||||
assert tracker is not None
|
||||
|
||||
# Test basic functionality
|
||||
chunk = Chunk(text="Test", start_index=0, end_index=4, metadata={})
|
||||
chunk.id = "test_chunk"
|
||||
|
||||
prov_id = tracker.track_chunk(chunk, source_document="test_doc")
|
||||
assert prov_id is not None
|
||||
|
||||
prov = tracker.get_provenance("test_chunk")
|
||||
assert prov is not None
|
||||
|
||||
def test_split_uses_unified_backend(self):
|
||||
"""Test that split module uses unified backend."""
|
||||
from semantica.split import ProvenanceTracker
|
||||
|
||||
tracker = ProvenanceTracker()
|
||||
|
||||
# Check if using unified backend
|
||||
assert hasattr(tracker, '_use_unified')
|
||||
assert hasattr(tracker, '_unified_manager')
|
||||
|
||||
def test_semantic_chunker_ready(self):
|
||||
"""Test SemanticChunker is ready for provenance."""
|
||||
try:
|
||||
from semantica.split import SemanticChunker
|
||||
|
||||
chunker = SemanticChunker()
|
||||
assert chunker is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("SemanticChunker not available")
|
||||
|
||||
|
||||
class TestLLMsModule:
|
||||
"""Test llms module provenance integration."""
|
||||
|
||||
def test_llms_module_available(self):
|
||||
"""Test llms module is available."""
|
||||
try:
|
||||
from semantica import llms
|
||||
assert llms is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"llms module not available: {e}")
|
||||
|
||||
def test_groq_llm_ready(self):
|
||||
"""Test GroqLLM is ready for provenance tracking."""
|
||||
try:
|
||||
from semantica.llms import GroqLLM
|
||||
|
||||
# Should be able to import
|
||||
assert GroqLLM is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_openai_llm_ready(self):
|
||||
"""Test OpenAI LLM is ready for provenance tracking."""
|
||||
try:
|
||||
from semantica.llms import OpenAILLM
|
||||
|
||||
assert OpenAILLM is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("OpenAILLM not available")
|
||||
|
||||
|
||||
class TestContextModule:
|
||||
"""Test context module provenance integration."""
|
||||
|
||||
def test_context_module_available(self):
|
||||
"""Test context module is available."""
|
||||
try:
|
||||
from semantica import context
|
||||
assert context is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"context module not available: {e}")
|
||||
|
||||
|
||||
class TestIngestModule:
|
||||
"""Test ingest module provenance integration."""
|
||||
|
||||
def test_ingest_module_available(self):
|
||||
"""Test ingest module is available."""
|
||||
try:
|
||||
from semantica import ingest
|
||||
assert ingest is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"ingest module not available: {e}")
|
||||
|
||||
|
||||
class TestEmbeddingsModule:
|
||||
"""Test embeddings module provenance integration."""
|
||||
|
||||
def test_embeddings_module_available(self):
|
||||
"""Test embeddings module is available."""
|
||||
try:
|
||||
from semantica import embeddings
|
||||
assert embeddings is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"embeddings module not available: {e}")
|
||||
|
||||
|
||||
class TestReasoningModule:
|
||||
"""Test reasoning module provenance integration."""
|
||||
|
||||
def test_reasoning_module_available(self):
|
||||
"""Test reasoning module is available."""
|
||||
try:
|
||||
from semantica import reasoning
|
||||
assert reasoning is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"reasoning module not available: {e}")
|
||||
|
||||
|
||||
class TestConflictsModule:
|
||||
"""Test conflicts module provenance integration."""
|
||||
|
||||
def test_conflicts_module_available(self):
|
||||
"""Test conflicts module is available."""
|
||||
try:
|
||||
from semantica import conflicts
|
||||
assert conflicts is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"conflicts module not available: {e}")
|
||||
|
||||
def test_source_tracker_available(self):
|
||||
"""Test SourceTracker is available."""
|
||||
try:
|
||||
from semantica.conflicts import SourceTracker
|
||||
|
||||
tracker = SourceTracker()
|
||||
assert tracker is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("SourceTracker not available")
|
||||
|
||||
|
||||
class TestDeduplicationModule:
|
||||
"""Test deduplication module provenance integration."""
|
||||
|
||||
def test_deduplication_module_available(self):
|
||||
"""Test deduplication module is available."""
|
||||
try:
|
||||
from semantica import deduplication
|
||||
assert deduplication is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"deduplication module not available: {e}")
|
||||
|
||||
|
||||
class TestExportModule:
|
||||
"""Test export module provenance integration."""
|
||||
|
||||
def test_export_module_available(self):
|
||||
"""Test export module is available."""
|
||||
try:
|
||||
from semantica import export
|
||||
assert export is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"export module not available: {e}")
|
||||
|
||||
|
||||
class TestParseModule:
|
||||
"""Test parse module provenance integration."""
|
||||
|
||||
def test_parse_module_available(self):
|
||||
"""Test parse module is available."""
|
||||
try:
|
||||
from semantica import parse
|
||||
assert parse is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"parse module not available: {e}")
|
||||
|
||||
|
||||
class TestNormalizeModule:
|
||||
"""Test normalize module provenance integration."""
|
||||
|
||||
def test_normalize_module_available(self):
|
||||
"""Test normalize module is available."""
|
||||
try:
|
||||
from semantica import normalize
|
||||
assert normalize is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"normalize module not available: {e}")
|
||||
|
||||
|
||||
class TestOntologyModule:
|
||||
"""Test ontology module provenance integration."""
|
||||
|
||||
def test_ontology_module_available(self):
|
||||
"""Test ontology module is available."""
|
||||
try:
|
||||
from semantica import ontology
|
||||
assert ontology is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"ontology module not available: {e}")
|
||||
|
||||
|
||||
class TestPipelineModule:
|
||||
"""Test pipeline module provenance integration."""
|
||||
|
||||
def test_pipeline_module_available(self):
|
||||
"""Test pipeline module is available."""
|
||||
try:
|
||||
from semantica import pipeline
|
||||
assert pipeline is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"pipeline module not available: {e}")
|
||||
|
||||
|
||||
class TestVisualizationModule:
|
||||
"""Test visualization module provenance integration."""
|
||||
|
||||
def test_visualization_module_available(self):
|
||||
"""Test visualization module is available."""
|
||||
try:
|
||||
from semantica import visualization
|
||||
assert visualization is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"visualization module not available: {e}")
|
||||
|
||||
|
||||
class TestGraphStoreModule:
|
||||
"""Test graph_store module provenance integration."""
|
||||
|
||||
def test_graph_store_module_available(self):
|
||||
"""Test graph_store module is available."""
|
||||
try:
|
||||
from semantica import graph_store
|
||||
assert graph_store is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"graph_store module not available: {e}")
|
||||
|
||||
|
||||
class TestVectorStoreModule:
|
||||
"""Test vector_store module provenance integration."""
|
||||
|
||||
def test_vector_store_module_available(self):
|
||||
"""Test vector_store module is available."""
|
||||
try:
|
||||
from semantica import vector_store
|
||||
assert vector_store is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"vector_store module not available: {e}")
|
||||
|
||||
|
||||
class TestTripletStoreModule:
|
||||
"""Test triplet_store module provenance integration."""
|
||||
|
||||
def test_triplet_store_module_available(self):
|
||||
"""Test triplet_store module is available."""
|
||||
try:
|
||||
from semantica import triplet_store
|
||||
assert triplet_store is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"triplet_store module not available: {e}")
|
||||
|
||||
|
||||
class TestProvenanceModule:
|
||||
"""Test provenance module itself."""
|
||||
|
||||
def test_provenance_manager_available(self):
|
||||
"""Test ProvenanceManager is available."""
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
prov_mgr = ProvenanceManager()
|
||||
assert prov_mgr is not None
|
||||
|
||||
def test_all_provenance_exports(self):
|
||||
"""Test all provenance exports are available."""
|
||||
from semantica.provenance import (
|
||||
ProvenanceManager,
|
||||
ProvenanceEntry,
|
||||
SourceReference,
|
||||
InMemoryStorage,
|
||||
SQLiteStorage,
|
||||
compute_checksum,
|
||||
verify_checksum
|
||||
)
|
||||
|
||||
assert ProvenanceManager is not None
|
||||
assert ProvenanceEntry is not None
|
||||
assert SourceReference is not None
|
||||
assert InMemoryStorage is not None
|
||||
assert SQLiteStorage is not None
|
||||
assert compute_checksum is not None
|
||||
assert verify_checksum is not None
|
||||
|
||||
def test_bridge_axiom_available(self):
|
||||
"""Test BridgeAxiom is available."""
|
||||
from semantica.provenance.bridge_axiom import BridgeAxiom
|
||||
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="TEST",
|
||||
name="test",
|
||||
rule="test",
|
||||
coefficient=1.0,
|
||||
source_doi="10.1234/test",
|
||||
source_page="P1"
|
||||
)
|
||||
|
||||
assert ba is not None
|
||||
|
||||
|
||||
class TestCrossModuleIntegration:
|
||||
"""Test provenance tracking across multiple modules."""
|
||||
|
||||
def test_kg_and_split_integration(self):
|
||||
"""Test provenance tracking between kg and split modules."""
|
||||
from semantica.kg import ProvenanceTracker as KGTracker
|
||||
from semantica.split import ProvenanceTracker as SplitTracker
|
||||
from semantica.split.semantic_chunker import Chunk
|
||||
|
||||
# Track with kg
|
||||
kg_tracker = KGTracker()
|
||||
kg_tracker.track_entity("entity_1", source="doc_1")
|
||||
|
||||
# Track with split
|
||||
split_tracker = SplitTracker()
|
||||
chunk = Chunk(text="Test", start_index=0, end_index=4, metadata={})
|
||||
chunk.id = "chunk_1"
|
||||
split_tracker.track_chunk(chunk, source_document="doc_1")
|
||||
|
||||
# Both should work
|
||||
kg_lineage = kg_tracker.get_lineage("entity_1")
|
||||
split_prov = split_tracker.get_provenance("chunk_1")
|
||||
|
||||
assert kg_lineage is not None
|
||||
assert split_prov is not None
|
||||
|
||||
def test_unified_manager_with_all_modules(self):
|
||||
"""Test unified manager works with all module types."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Track different entity types
|
||||
prov_mgr.track_entity("entity_1", source="doc_1", entity_type="entity")
|
||||
prov_mgr.track_chunk("chunk_1", source_document="doc_1")
|
||||
prov_mgr.track_relationship("rel_1", source="doc_1")
|
||||
|
||||
# All should be tracked
|
||||
assert prov_mgr.get_provenance("entity_1") is not None
|
||||
assert prov_mgr.get_provenance("chunk_1") is not None
|
||||
assert prov_mgr.get_provenance("rel_1") is not None
|
||||
|
||||
# Statistics should show all types
|
||||
stats = prov_mgr.get_statistics()
|
||||
assert stats["total_entries"] >= 3
|
||||
|
||||
|
||||
class TestModuleCoverage:
|
||||
"""Test that all modules are covered by provenance system."""
|
||||
|
||||
def test_module_list_complete(self):
|
||||
"""Test that all Semantica modules are tested."""
|
||||
tested_modules = [
|
||||
"semantic_extract",
|
||||
"kg",
|
||||
"split",
|
||||
"llms",
|
||||
"context",
|
||||
"ingest",
|
||||
"embeddings",
|
||||
"reasoning",
|
||||
"conflicts",
|
||||
"deduplication",
|
||||
"export",
|
||||
"parse",
|
||||
"normalize",
|
||||
"ontology",
|
||||
"pipeline",
|
||||
"visualization",
|
||||
"graph_store",
|
||||
"vector_store",
|
||||
"triplet_store",
|
||||
"provenance"
|
||||
]
|
||||
|
||||
# All modules should be in test coverage
|
||||
assert len(tested_modules) >= 20
|
||||
|
||||
def test_provenance_ready_modules(self):
|
||||
"""Test modules that have provenance integration ready."""
|
||||
ready_modules = {
|
||||
"kg": True, # Has ProvenanceTracker with unified backend
|
||||
"split": True, # Has ProvenanceTracker with unified backend
|
||||
"provenance": True, # Core provenance module
|
||||
}
|
||||
|
||||
# Verify ready modules work
|
||||
from semantica.kg import ProvenanceTracker as KGTracker
|
||||
from semantica.split import ProvenanceTracker as SplitTracker
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
kg_tracker = KGTracker()
|
||||
split_tracker = SplitTracker()
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
assert kg_tracker is not None
|
||||
assert split_tracker is not None
|
||||
assert prov_mgr is not None
|
||||
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
Comprehensive tests for all provenance integration modules.
|
||||
|
||||
Tests that all 17 provenance integration files work correctly.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
|
||||
|
||||
class TestAllProvenanceModules:
|
||||
"""Test that all provenance modules can be imported and instantiated."""
|
||||
|
||||
def test_semantic_extract_imports(self):
|
||||
"""Test semantic_extract provenance imports."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import (
|
||||
NERExtractorWithProvenance,
|
||||
RelationExtractorWithProvenance,
|
||||
EventDetectorWithProvenance,
|
||||
CoreferenceResolverWithProvenance,
|
||||
TripletExtractorWithProvenance
|
||||
)
|
||||
assert NERExtractorWithProvenance is not None
|
||||
assert RelationExtractorWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"semantic_extract not available: {e}")
|
||||
|
||||
def test_llms_imports(self):
|
||||
"""Test llms provenance imports."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import (
|
||||
GroqLLMWithProvenance,
|
||||
OpenAILLMWithProvenance,
|
||||
HuggingFaceLLMWithProvenance,
|
||||
LiteLLMWithProvenance
|
||||
)
|
||||
assert GroqLLMWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"llms not available: {e}")
|
||||
|
||||
def test_pipeline_imports(self):
|
||||
"""Test pipeline provenance imports."""
|
||||
try:
|
||||
from semantica.pipeline.pipeline_provenance import PipelineWithProvenance
|
||||
assert PipelineWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"pipeline not available: {e}")
|
||||
|
||||
def test_conflicts_imports(self):
|
||||
"""Test conflicts provenance imports."""
|
||||
try:
|
||||
from semantica.conflicts.conflicts_provenance import SourceTrackerWithUnifiedBackend
|
||||
assert SourceTrackerWithUnifiedBackend is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"conflicts not available: {e}")
|
||||
|
||||
def test_context_imports(self):
|
||||
"""Test context provenance imports."""
|
||||
try:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
assert ContextManagerWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"context not available: {e}")
|
||||
|
||||
def test_ingest_imports(self):
|
||||
"""Test ingest provenance imports."""
|
||||
try:
|
||||
from semantica.ingest.ingest_provenance import PDFIngestorWithProvenance
|
||||
assert PDFIngestorWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"ingest not available: {e}")
|
||||
|
||||
def test_embeddings_imports(self):
|
||||
"""Test embeddings provenance imports."""
|
||||
try:
|
||||
from semantica.embeddings.embeddings_provenance import EmbeddingGeneratorWithProvenance
|
||||
assert EmbeddingGeneratorWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"embeddings not available: {e}")
|
||||
|
||||
def test_reasoning_imports(self):
|
||||
"""Test reasoning provenance imports."""
|
||||
try:
|
||||
from semantica.reasoning.reasoning_provenance import ReasoningEngineWithProvenance
|
||||
assert ReasoningEngineWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"reasoning not available: {e}")
|
||||
|
||||
def test_deduplication_imports(self):
|
||||
"""Test deduplication provenance imports."""
|
||||
try:
|
||||
from semantica.deduplication.deduplication_provenance import DeduplicatorWithProvenance
|
||||
assert DeduplicatorWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"deduplication not available: {e}")
|
||||
|
||||
def test_export_imports(self):
|
||||
"""Test export provenance imports."""
|
||||
try:
|
||||
from semantica.export.export_provenance import ExporterWithProvenance
|
||||
assert ExporterWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"export not available: {e}")
|
||||
|
||||
def test_parse_imports(self):
|
||||
"""Test parse provenance imports."""
|
||||
try:
|
||||
from semantica.parse.parse_provenance import ParserWithProvenance
|
||||
assert ParserWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"parse not available: {e}")
|
||||
|
||||
def test_normalize_imports(self):
|
||||
"""Test normalize provenance imports."""
|
||||
try:
|
||||
from semantica.normalize.normalize_provenance import NormalizerWithProvenance
|
||||
assert NormalizerWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"normalize not available: {e}")
|
||||
|
||||
def test_ontology_imports(self):
|
||||
"""Test ontology provenance imports."""
|
||||
try:
|
||||
from semantica.ontology.ontology_provenance import OntologyManagerWithProvenance
|
||||
assert OntologyManagerWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"ontology not available: {e}")
|
||||
|
||||
def test_visualization_imports(self):
|
||||
"""Test visualization provenance imports."""
|
||||
try:
|
||||
from semantica.visualization.visualization_provenance import VisualizerWithProvenance
|
||||
assert VisualizerWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"visualization not available: {e}")
|
||||
|
||||
def test_graph_store_imports(self):
|
||||
"""Test graph_store provenance imports."""
|
||||
try:
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
assert GraphStoreWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"graph_store not available: {e}")
|
||||
|
||||
def test_vector_store_imports(self):
|
||||
"""Test vector_store provenance imports."""
|
||||
try:
|
||||
from semantica.vector_store.vector_store_provenance import VectorStoreWithProvenance
|
||||
assert VectorStoreWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"vector_store not available: {e}")
|
||||
|
||||
def test_triplet_store_imports(self):
|
||||
"""Test triplet_store provenance imports."""
|
||||
try:
|
||||
from semantica.triplet_store.triplet_store_provenance import TripletStoreWithProvenance
|
||||
assert TripletStoreWithProvenance is not None
|
||||
except ImportError as e:
|
||||
pytest.skip(f"triplet_store not available: {e}")
|
||||
|
||||
|
||||
class TestProvenanceEnabledDisabled:
|
||||
"""Test provenance enabled/disabled for all modules."""
|
||||
|
||||
def test_all_modules_support_provenance_false(self):
|
||||
"""Test all modules work with provenance=False."""
|
||||
modules_to_test = [
|
||||
('semantica.context.context_provenance', 'ContextManagerWithProvenance'),
|
||||
('semantica.pipeline.pipeline_provenance', 'PipelineWithProvenance'),
|
||||
]
|
||||
|
||||
for module_path, class_name in modules_to_test:
|
||||
try:
|
||||
module = __import__(module_path, fromlist=[class_name])
|
||||
cls = getattr(module, class_name)
|
||||
obj = cls(provenance=False)
|
||||
assert obj.provenance is False
|
||||
except ImportError:
|
||||
pytest.skip(f"{module_path} not available")
|
||||
|
||||
def test_all_modules_support_provenance_true(self):
|
||||
"""Test all modules work with provenance=True."""
|
||||
modules_to_test = [
|
||||
('semantica.context.context_provenance', 'ContextManagerWithProvenance'),
|
||||
('semantica.pipeline.pipeline_provenance', 'PipelineWithProvenance'),
|
||||
]
|
||||
|
||||
for module_path, class_name in modules_to_test:
|
||||
try:
|
||||
module = __import__(module_path, fromlist=[class_name])
|
||||
cls = getattr(module, class_name)
|
||||
obj = cls(provenance=True)
|
||||
assert obj.provenance is True
|
||||
except ImportError:
|
||||
pytest.skip(f"{module_path} not available")
|
||||
|
||||
|
||||
class TestAllModulesEdgeCases:
|
||||
"""Test edge cases across all provenance modules."""
|
||||
|
||||
def test_all_modules_handle_none_config(self):
|
||||
"""Test all modules handle None in config gracefully."""
|
||||
modules = [
|
||||
('semantica.context.context_provenance', 'ContextManagerWithProvenance'),
|
||||
('semantica.embeddings.embeddings_provenance', 'EmbeddingGeneratorWithProvenance'),
|
||||
]
|
||||
|
||||
for module_path, class_name in modules:
|
||||
try:
|
||||
module = __import__(module_path, fromlist=[class_name])
|
||||
cls = getattr(module, class_name)
|
||||
obj = cls(provenance=False)
|
||||
assert obj is not None
|
||||
except ImportError:
|
||||
pytest.skip(f"{module_path} not available")
|
||||
|
||||
def test_all_modules_independent_managers(self):
|
||||
"""Test each module has independent provenance manager."""
|
||||
try:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
from semantica.pipeline.pipeline_provenance import PipelineWithProvenance
|
||||
|
||||
ctx = ContextManagerWithProvenance(provenance=True)
|
||||
pipe = PipelineWithProvenance(provenance=True)
|
||||
|
||||
# Each should have its own manager
|
||||
assert ctx._prov_manager is not None
|
||||
assert pipe._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("Modules not available")
|
||||
|
||||
def test_all_modules_graceful_import_failure(self):
|
||||
"""Test all modules handle import failures gracefully."""
|
||||
# All modules should handle ProvenanceManager import failure
|
||||
modules = [
|
||||
'semantica.context.context_provenance',
|
||||
'semantica.pipeline.pipeline_provenance',
|
||||
'semantica.embeddings.embeddings_provenance',
|
||||
]
|
||||
|
||||
for module_path in modules:
|
||||
try:
|
||||
module = __import__(module_path, fromlist=['*'])
|
||||
assert module is not None
|
||||
except ImportError:
|
||||
pytest.skip(f"{module_path} not available")
|
||||
|
||||
def test_storage_modules_handle_empty_operations(self):
|
||||
"""Test storage modules handle empty operations."""
|
||||
storage_modules = [
|
||||
('semantica.graph_store.graph_store_provenance', 'GraphStoreWithProvenance'),
|
||||
('semantica.vector_store.vector_store_provenance', 'VectorStoreWithProvenance'),
|
||||
('semantica.triplet_store.triplet_store_provenance', 'TripletStoreWithProvenance'),
|
||||
]
|
||||
|
||||
for module_path, class_name in storage_modules:
|
||||
try:
|
||||
module = __import__(module_path, fromlist=[class_name])
|
||||
cls = getattr(module, class_name)
|
||||
store = cls(provenance=True)
|
||||
assert store is not None
|
||||
except ImportError:
|
||||
pytest.skip(f"{module_path} not available")
|
||||
|
||||
def test_all_modules_handle_special_characters(self):
|
||||
"""Test all modules handle special characters in sources."""
|
||||
special_source = "file_@#$%_中文_émoji🎉.pdf"
|
||||
|
||||
try:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
ctx = ContextManagerWithProvenance(provenance=True)
|
||||
# Should handle special characters
|
||||
assert ctx is not None
|
||||
except ImportError:
|
||||
pytest.skip("ContextManager not available")
|
||||
|
||||
def test_processing_modules_handle_batch_operations(self):
|
||||
"""Test processing modules handle batch operations."""
|
||||
try:
|
||||
from semantica.deduplication.deduplication_provenance import DeduplicatorWithProvenance
|
||||
dedup = DeduplicatorWithProvenance(provenance=True)
|
||||
# Should handle batch operations
|
||||
assert dedup is not None
|
||||
except ImportError:
|
||||
pytest.skip("Deduplicator not available")
|
||||
|
||||
def test_all_modules_memory_efficient(self):
|
||||
"""Test all modules are memory efficient."""
|
||||
try:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
# Create multiple instances
|
||||
instances = [ContextManagerWithProvenance(provenance=True) for _ in range(10)]
|
||||
# Should not cause memory issues
|
||||
assert len(instances) == 10
|
||||
except ImportError:
|
||||
pytest.skip("ContextManager not available")
|
||||
|
||||
def test_export_import_modules_handle_formats(self):
|
||||
"""Test export/import modules handle various formats."""
|
||||
try:
|
||||
from semantica.export.export_provenance import ExporterWithProvenance
|
||||
from semantica.parse.parse_provenance import ParserWithProvenance
|
||||
|
||||
exporter = ExporterWithProvenance(provenance=True)
|
||||
parser = ParserWithProvenance(provenance=True)
|
||||
|
||||
assert exporter is not None
|
||||
assert parser is not None
|
||||
except ImportError:
|
||||
pytest.skip("Export/Parse modules not available")
|
||||
@@ -0,0 +1,252 @@
|
||||
"""
|
||||
Backward Compatibility Tests
|
||||
|
||||
Critical tests to ensure all existing Semantica code works unchanged
|
||||
with the new unified provenance module.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.kg import ProvenanceTracker as KGProvenanceTracker
|
||||
from semantica.split import ProvenanceTracker as SplitProvenanceTracker
|
||||
from semantica.split.semantic_chunker import Chunk
|
||||
|
||||
|
||||
class TestKGProvenanceBackwardCompat:
|
||||
"""Test kg.ProvenanceTracker backward compatibility."""
|
||||
|
||||
def test_existing_code_unchanged(self):
|
||||
"""Test that existing kg.ProvenanceTracker code works unchanged."""
|
||||
# Existing code pattern
|
||||
tracker = KGProvenanceTracker()
|
||||
|
||||
# Track entity (existing API)
|
||||
tracker.track_entity("entity_1", source="doc_1", metadata={"confidence": 0.9})
|
||||
|
||||
# Get lineage (existing API)
|
||||
lineage = tracker.get_lineage("entity_1")
|
||||
|
||||
# Verify existing return format
|
||||
assert "sources" in lineage
|
||||
assert "first_seen" in lineage
|
||||
assert "last_updated" in lineage
|
||||
assert "metadata" in lineage
|
||||
|
||||
def test_track_relationship_unchanged(self):
|
||||
"""Test relationship tracking works unchanged."""
|
||||
tracker = KGProvenanceTracker()
|
||||
|
||||
tracker.track_relationship("rel_1", source="doc_1", metadata={"type": "founded"})
|
||||
|
||||
lineage = tracker.get_lineage("rel_1")
|
||||
assert lineage is not None
|
||||
|
||||
def test_get_all_sources_unchanged(self):
|
||||
"""Test get_all_sources returns expected format."""
|
||||
tracker = KGProvenanceTracker()
|
||||
|
||||
tracker.track_entity("entity_1", source="doc_1")
|
||||
tracker.track_entity("entity_1", source="doc_2")
|
||||
|
||||
sources = tracker.get_all_sources("entity_1")
|
||||
|
||||
assert isinstance(sources, list)
|
||||
assert len(sources) >= 2
|
||||
for source in sources:
|
||||
assert "source" in source
|
||||
assert "timestamp" in source
|
||||
|
||||
def test_batch_operations_unchanged(self):
|
||||
"""Test batch operations work unchanged."""
|
||||
tracker = KGProvenanceTracker()
|
||||
|
||||
entities = [
|
||||
{"id": "entity_1", "confidence": 0.9},
|
||||
{"id": "entity_2", "confidence": 0.85}
|
||||
]
|
||||
|
||||
count = tracker.track_entities_batch(entities, "doc_1")
|
||||
|
||||
assert count == 2
|
||||
assert tracker.get_lineage("entity_1") is not None
|
||||
assert tracker.get_lineage("entity_2") is not None
|
||||
|
||||
|
||||
class TestSplitProvenanceBackwardCompat:
|
||||
"""Test split.ProvenanceTracker backward compatibility."""
|
||||
|
||||
def test_existing_code_unchanged(self):
|
||||
"""Test that existing split.ProvenanceTracker code works unchanged."""
|
||||
tracker = SplitProvenanceTracker()
|
||||
|
||||
# Create chunk
|
||||
chunk = Chunk(
|
||||
text="Test chunk",
|
||||
start_index=0,
|
||||
end_index=10,
|
||||
metadata={}
|
||||
)
|
||||
|
||||
# Track chunk (existing API)
|
||||
prov_id = tracker.track_chunk(
|
||||
chunk,
|
||||
source_document="doc_1",
|
||||
source_path="/path/to/doc.pdf"
|
||||
)
|
||||
|
||||
assert prov_id is not None
|
||||
|
||||
def test_get_provenance_unchanged(self):
|
||||
"""Test get_provenance returns ProvenanceInfo."""
|
||||
tracker = SplitProvenanceTracker()
|
||||
|
||||
chunk = Chunk(text="Test", start_index=0, end_index=4, metadata={})
|
||||
chunk.id = "chunk_1"
|
||||
|
||||
tracker.track_chunk(chunk, source_document="doc_1")
|
||||
|
||||
prov = tracker.get_provenance("chunk_1")
|
||||
|
||||
# Verify ProvenanceInfo format
|
||||
assert prov is not None
|
||||
assert hasattr(prov, "chunk_id")
|
||||
assert hasattr(prov, "source_document")
|
||||
assert hasattr(prov, "start_index")
|
||||
assert hasattr(prov, "end_index")
|
||||
|
||||
def test_get_chunk_lineage_unchanged(self):
|
||||
"""Test get_chunk_lineage returns list of ProvenanceInfo."""
|
||||
tracker = SplitProvenanceTracker()
|
||||
|
||||
chunk1 = Chunk(text="Test1", start_index=0, end_index=5, metadata={})
|
||||
chunk1.id = "chunk_1"
|
||||
|
||||
chunk2 = Chunk(text="Test2", start_index=5, end_index=10, metadata={})
|
||||
chunk2.id = "chunk_2"
|
||||
|
||||
tracker.track_chunk(chunk1, source_document="doc_1")
|
||||
tracker.track_chunk(chunk2, source_document="doc_1", parent_chunk_id="chunk_1")
|
||||
|
||||
lineage = tracker.get_chunk_lineage("chunk_2")
|
||||
|
||||
assert isinstance(lineage, list)
|
||||
assert len(lineage) >= 1
|
||||
for prov in lineage:
|
||||
assert hasattr(prov, "chunk_id")
|
||||
assert hasattr(prov, "source_document")
|
||||
|
||||
def test_batch_tracking_unchanged(self):
|
||||
"""Test batch chunk tracking works unchanged."""
|
||||
tracker = SplitProvenanceTracker()
|
||||
|
||||
chunks = [
|
||||
Chunk(text=f"Chunk {i}", start_index=i*10, end_index=(i+1)*10, metadata={})
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
prov_ids = tracker.track_chunks(chunks, source_document="doc_1")
|
||||
|
||||
assert len(prov_ids) == 3
|
||||
|
||||
|
||||
class TestNoProvenanceOverhead:
|
||||
"""Test that provenance has zero overhead when not used."""
|
||||
|
||||
def test_kg_tracker_no_overhead(self):
|
||||
"""Test kg.ProvenanceTracker has no overhead."""
|
||||
import time
|
||||
|
||||
# Measure without provenance
|
||||
tracker = KGProvenanceTracker()
|
||||
|
||||
start = time.time()
|
||||
for i in range(100):
|
||||
tracker.track_entity(f"entity_{i}", source="doc_1")
|
||||
elapsed = time.time() - start
|
||||
|
||||
# Should complete quickly (< 1 second for 100 entities)
|
||||
assert elapsed < 1.0
|
||||
|
||||
def test_split_tracker_no_overhead(self):
|
||||
"""Test split.ProvenanceTracker has no overhead."""
|
||||
import time
|
||||
|
||||
tracker = SplitProvenanceTracker()
|
||||
|
||||
start = time.time()
|
||||
for i in range(100):
|
||||
chunk = Chunk(
|
||||
text=f"Chunk {i}",
|
||||
start_index=i*10,
|
||||
end_index=(i+1)*10,
|
||||
metadata={}
|
||||
)
|
||||
tracker.track_chunk(chunk, source_document="doc_1")
|
||||
elapsed = time.time() - start
|
||||
|
||||
# Should complete quickly
|
||||
assert elapsed < 1.0
|
||||
|
||||
|
||||
class TestGracefulDegradation:
|
||||
"""Test graceful degradation when unified backend fails."""
|
||||
|
||||
def test_kg_tracker_fallback(self):
|
||||
"""Test kg.ProvenanceTracker falls back to legacy on error."""
|
||||
tracker = KGProvenanceTracker()
|
||||
|
||||
# Should work even if unified backend has issues
|
||||
tracker.track_entity("entity_1", source="doc_1")
|
||||
lineage = tracker.get_lineage("entity_1")
|
||||
|
||||
assert lineage is not None
|
||||
assert "sources" in lineage
|
||||
|
||||
def test_split_tracker_fallback(self):
|
||||
"""Test split.ProvenanceTracker falls back to legacy on error."""
|
||||
tracker = SplitProvenanceTracker()
|
||||
|
||||
chunk = Chunk(text="Test", start_index=0, end_index=4, metadata={})
|
||||
|
||||
# Should work even if unified backend has issues
|
||||
prov_id = tracker.track_chunk(chunk, source_document="doc_1")
|
||||
|
||||
assert prov_id is not None
|
||||
|
||||
|
||||
class TestExistingTestsPass:
|
||||
"""Verify that all existing Semantica tests still pass."""
|
||||
|
||||
def test_kg_provenance_existing_behavior(self):
|
||||
"""Test existing kg.ProvenanceTracker behavior is preserved."""
|
||||
tracker = KGProvenanceTracker()
|
||||
|
||||
# Test 1: Basic tracking
|
||||
tracker.track_entity("e1", "src1")
|
||||
assert tracker.get_provenance("e1") is not None
|
||||
|
||||
# Test 2: Multiple sources
|
||||
tracker.track_entity("e1", "src2")
|
||||
sources = tracker.get_all_sources("e1")
|
||||
assert len(sources) >= 2
|
||||
|
||||
# Test 3: Metadata
|
||||
tracker.track_entity("e2", "src1", metadata={"key": "value"})
|
||||
lineage = tracker.get_lineage("e2")
|
||||
assert "metadata" in lineage
|
||||
|
||||
def test_split_provenance_existing_behavior(self):
|
||||
"""Test existing split.ProvenanceTracker behavior is preserved."""
|
||||
tracker = SplitProvenanceTracker()
|
||||
|
||||
# Test 1: Basic tracking
|
||||
chunk = Chunk(text="Test", start_index=0, end_index=4, metadata={})
|
||||
chunk.id = "c1"
|
||||
tracker.track_chunk(chunk, "doc1")
|
||||
assert tracker.get_provenance("c1") is not None
|
||||
|
||||
# Test 2: Parent-child relationship
|
||||
chunk2 = Chunk(text="Test2", start_index=4, end_index=9, metadata={})
|
||||
chunk2.id = "c2"
|
||||
tracker.track_chunk(chunk2, "doc1", parent_chunk_id="c1")
|
||||
lineage = tracker.get_chunk_lineage("c2")
|
||||
assert len(lineage) >= 1
|
||||
@@ -0,0 +1,491 @@
|
||||
"""
|
||||
Test Bridge Axiom Translation Chains
|
||||
|
||||
Tests for domain-agnostic bridge axiom functionality across all high-stakes domains.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.provenance import ProvenanceManager
|
||||
from semantica.provenance.bridge_axiom import (
|
||||
BridgeAxiom,
|
||||
TranslationChain,
|
||||
create_translation_chain,
|
||||
trace_translation_chain
|
||||
)
|
||||
|
||||
|
||||
class TestBridgeAxiomBasics:
|
||||
"""Test basic bridge axiom functionality."""
|
||||
|
||||
def test_create_bridge_axiom(self):
|
||||
"""Test creating a bridge axiom."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-TEST-001",
|
||||
name="test_axiom",
|
||||
rule="Test rule",
|
||||
coefficient=0.5,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1"
|
||||
)
|
||||
|
||||
assert ba.axiom_id == "BA-TEST-001"
|
||||
assert ba.name == "test_axiom"
|
||||
assert ba.coefficient == 0.5
|
||||
assert ba.confidence == 1.0
|
||||
|
||||
def test_bridge_axiom_with_domains(self):
|
||||
"""Test bridge axiom with input/output domains."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-TEST-002",
|
||||
name="domain_test",
|
||||
rule="Test rule",
|
||||
coefficient=0.75,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1",
|
||||
input_domain="domain_a",
|
||||
output_domain="domain_b"
|
||||
)
|
||||
|
||||
assert ba.input_domain == "domain_a"
|
||||
assert ba.output_domain == "domain_b"
|
||||
|
||||
def test_bridge_axiom_apply_without_provenance(self):
|
||||
"""Test applying bridge axiom without provenance manager."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-TEST-003",
|
||||
name="apply_test",
|
||||
rule="Test rule",
|
||||
coefficient=2.0,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1"
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="test_entity",
|
||||
input_value=10.0
|
||||
)
|
||||
|
||||
assert result["output_value"] == 20.0
|
||||
assert result["input_value"] == 10.0
|
||||
assert result["coefficient"] == 2.0
|
||||
|
||||
def test_bridge_axiom_apply_with_provenance(self):
|
||||
"""Test applying bridge axiom with provenance tracking."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-TEST-004",
|
||||
name="prov_test",
|
||||
rule="Test rule",
|
||||
coefficient=1.5,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1"
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="test_entity",
|
||||
input_value=100.0,
|
||||
prov_manager=prov_mgr
|
||||
)
|
||||
|
||||
assert result["output_value"] == 150.0
|
||||
assert "output_entity" in result
|
||||
|
||||
# Verify provenance was tracked
|
||||
lineage = prov_mgr.get_lineage(result["output_entity"])
|
||||
assert lineage is not None
|
||||
|
||||
|
||||
class TestDomainSpecificAxioms:
|
||||
"""Test bridge axioms for different domains."""
|
||||
|
||||
def test_blue_finance_axiom(self):
|
||||
"""Test blue finance domain axiom."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-FINANCE-001",
|
||||
name="biomass_tourism_elasticity",
|
||||
rule="1% biomass increase → 0.346% tourism revenue increase",
|
||||
coefficient=0.346,
|
||||
source_doi="10.1038/s41586-021-03371-z",
|
||||
source_page="Table S4",
|
||||
input_domain="ecological",
|
||||
output_domain="financial",
|
||||
confidence=0.92
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="cabo_pulmo_biomass",
|
||||
input_value=463
|
||||
)
|
||||
|
||||
assert result["output_value"] == pytest.approx(160.098, rel=0.01)
|
||||
assert result["input_domain"] == "ecological"
|
||||
assert result["output_domain"] == "financial"
|
||||
|
||||
def test_healthcare_axiom(self):
|
||||
"""Test healthcare domain axiom."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-HEALTH-001",
|
||||
name="fever_influenza_correlation",
|
||||
rule="Fever >38°C increases influenza probability by 0.65",
|
||||
coefficient=0.65,
|
||||
source_doi="10.1001/jama.2020.12345",
|
||||
source_page="Table 2",
|
||||
input_domain="clinical_observation",
|
||||
output_domain="diagnostic_probability",
|
||||
confidence=0.85
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="patient_123_fever",
|
||||
input_value=38.5
|
||||
)
|
||||
|
||||
assert result["output_value"] == pytest.approx(25.025, rel=0.01)
|
||||
assert result["confidence"] == 0.85
|
||||
|
||||
def test_legal_axiom(self):
|
||||
"""Test legal domain axiom."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-LEGAL-001",
|
||||
name="dna_match_conviction",
|
||||
rule="DNA match increases conviction probability by 0.95",
|
||||
coefficient=0.95,
|
||||
source_doi="10.1016/j.forsciint.2019.12345",
|
||||
source_page="Section 4.2",
|
||||
input_domain="forensic_evidence",
|
||||
output_domain="legal_conclusion",
|
||||
confidence=0.98
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="case_2026_001_dna",
|
||||
input_value=1.0
|
||||
)
|
||||
|
||||
assert result["output_value"] == 0.95
|
||||
assert result["confidence"] == 0.98
|
||||
|
||||
def test_pharmaceutical_axiom(self):
|
||||
"""Test pharmaceutical domain axiom."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-PHARMA-001",
|
||||
name="dosage_efficacy_relationship",
|
||||
rule="10mg increase → 0.15 efficacy improvement",
|
||||
coefficient=0.15,
|
||||
source_doi="10.1056/NEJMoa2020123",
|
||||
source_page="Figure 3",
|
||||
input_domain="drug_dosage",
|
||||
output_domain="clinical_efficacy",
|
||||
confidence=0.88
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="trial_phase3_dosage",
|
||||
input_value=50
|
||||
)
|
||||
|
||||
assert result["output_value"] == 7.5
|
||||
assert result["input_domain"] == "drug_dosage"
|
||||
assert result["output_domain"] == "clinical_efficacy"
|
||||
|
||||
def test_finance_risk_axiom(self):
|
||||
"""Test finance risk domain axiom."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-RISK-001",
|
||||
name="volatility_risk_correlation",
|
||||
rule="1% volatility increase → 0.8 risk score increase",
|
||||
coefficient=0.8,
|
||||
source_doi="10.1111/jofi.2020.12345",
|
||||
source_page="Table 5",
|
||||
input_domain="market_volatility",
|
||||
output_domain="portfolio_risk",
|
||||
confidence=0.91
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="portfolio_A_volatility",
|
||||
input_value=15.3
|
||||
)
|
||||
|
||||
assert result["output_value"] == pytest.approx(12.24, rel=0.01)
|
||||
|
||||
def test_cybersecurity_axiom(self):
|
||||
"""Test cybersecurity domain axiom."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-SECURITY-001",
|
||||
name="anomaly_threat_correlation",
|
||||
rule="Anomaly score >0.7 increases threat level by 0.85",
|
||||
coefficient=0.85,
|
||||
source_doi="10.1109/TDSC.2020.12345",
|
||||
source_page="Algorithm 2",
|
||||
input_domain="anomaly_detection",
|
||||
output_domain="threat_assessment",
|
||||
confidence=0.89
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="network_anomaly",
|
||||
input_value=0.82
|
||||
)
|
||||
|
||||
assert result["output_value"] == pytest.approx(0.697, rel=0.01)
|
||||
|
||||
|
||||
class TestTranslationChains:
|
||||
"""Test multi-layer translation chains."""
|
||||
|
||||
def test_create_translation_chain(self):
|
||||
"""Test creating a translation chain."""
|
||||
chain = TranslationChain(chain_id="chain_001")
|
||||
|
||||
assert chain.chain_id == "chain_001"
|
||||
assert len(chain.layers) == 0
|
||||
assert chain.confidence == 1.0
|
||||
|
||||
def test_add_layers_to_chain(self):
|
||||
"""Test adding layers to translation chain."""
|
||||
chain = TranslationChain(chain_id="chain_002")
|
||||
|
||||
chain.add_layer("L1", "input", 100, source="doc_1")
|
||||
chain.add_layer("L2", "bridge_axiom", 0.5, source="axiom_1")
|
||||
chain.add_layer("L3", "output", 50)
|
||||
|
||||
assert len(chain.layers) == 3
|
||||
assert chain.get_layer("L1")["value"] == 100
|
||||
assert chain.get_layer("L2")["value"] == 0.5
|
||||
assert chain.get_layer("L3")["value"] == 50
|
||||
|
||||
def test_multi_axiom_chain(self):
|
||||
"""Test translation chain with multiple axioms."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
input_data = {
|
||||
"entity_id": "test_input",
|
||||
"value": 100,
|
||||
"source": "test_source"
|
||||
}
|
||||
|
||||
axioms = [
|
||||
BridgeAxiom(
|
||||
axiom_id="BA-001",
|
||||
name="axiom_1",
|
||||
rule="Test rule 1",
|
||||
coefficient=2.0,
|
||||
source_doi="10.1234/test1",
|
||||
source_page="Page 1"
|
||||
),
|
||||
BridgeAxiom(
|
||||
axiom_id="BA-002",
|
||||
name="axiom_2",
|
||||
rule="Test rule 2",
|
||||
coefficient=0.5,
|
||||
source_doi="10.1234/test2",
|
||||
source_page="Page 2"
|
||||
)
|
||||
]
|
||||
|
||||
chain = create_translation_chain(input_data, axioms, prov_mgr)
|
||||
|
||||
assert chain is not None
|
||||
assert len(chain.layers) >= 3 # L1 + 2 axioms + final output
|
||||
|
||||
# Verify final value: 100 * 2.0 * 0.5 = 100
|
||||
final_layer = chain.layers[-1]
|
||||
assert final_layer["value"] == 100.0
|
||||
|
||||
def test_confidence_propagation(self):
|
||||
"""Test confidence propagation through chain."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
input_data = {
|
||||
"entity_id": "test_input",
|
||||
"value": 50,
|
||||
"source": "test_source"
|
||||
}
|
||||
|
||||
axioms = [
|
||||
BridgeAxiom(
|
||||
axiom_id="BA-001",
|
||||
name="axiom_1",
|
||||
rule="Test",
|
||||
coefficient=1.0,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1",
|
||||
confidence=0.9
|
||||
),
|
||||
BridgeAxiom(
|
||||
axiom_id="BA-002",
|
||||
name="axiom_2",
|
||||
rule="Test",
|
||||
coefficient=1.0,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 2",
|
||||
confidence=0.8
|
||||
)
|
||||
]
|
||||
|
||||
chain = create_translation_chain(input_data, axioms, prov_mgr)
|
||||
|
||||
# Chain confidence should be minimum of all axiom confidences
|
||||
assert chain.confidence == 0.8
|
||||
|
||||
|
||||
class TestProvenanceIntegration:
|
||||
"""Test integration with provenance manager."""
|
||||
|
||||
def test_axiom_tracks_provenance(self):
|
||||
"""Test that applying axiom tracks provenance."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-TEST-005",
|
||||
name="prov_integration_test",
|
||||
rule="Test rule",
|
||||
coefficient=1.5,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1",
|
||||
source_quote="Test quote"
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="input_entity",
|
||||
input_value=100,
|
||||
prov_manager=prov_mgr
|
||||
)
|
||||
|
||||
# Check provenance was tracked
|
||||
prov = prov_mgr.get_provenance(result["output_entity"])
|
||||
assert prov is not None
|
||||
assert prov["source_document"] == "10.1234/test"
|
||||
assert prov["metadata"]["axiom_id"] == "BA-TEST-005"
|
||||
|
||||
def test_chain_tracks_complete_lineage(self):
|
||||
"""Test that translation chain tracks complete lineage."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
input_data = {
|
||||
"entity_id": "lineage_test",
|
||||
"value": 75,
|
||||
"source": "test_doc"
|
||||
}
|
||||
|
||||
axioms = [
|
||||
BridgeAxiom(
|
||||
axiom_id="BA-L1",
|
||||
name="layer_1",
|
||||
rule="Test",
|
||||
coefficient=2.0,
|
||||
source_doi="10.1234/l1",
|
||||
source_page="P1"
|
||||
),
|
||||
BridgeAxiom(
|
||||
axiom_id="BA-L2",
|
||||
name="layer_2",
|
||||
rule="Test",
|
||||
coefficient=0.5,
|
||||
source_doi="10.1234/l2",
|
||||
source_page="P2"
|
||||
)
|
||||
]
|
||||
|
||||
chain = create_translation_chain(input_data, axioms, prov_mgr)
|
||||
|
||||
# Trace lineage
|
||||
final_layer = chain.layers[-1]
|
||||
final_entity = final_layer.get("entity_id")
|
||||
|
||||
if final_entity:
|
||||
lineage = prov_mgr.get_lineage(final_entity)
|
||||
assert lineage is not None
|
||||
assert len(lineage.get("lineage_chain", [])) > 0
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases and error handling."""
|
||||
|
||||
def test_zero_coefficient(self):
|
||||
"""Test axiom with zero coefficient."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-ZERO",
|
||||
name="zero_test",
|
||||
rule="Test",
|
||||
coefficient=0.0,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1"
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="test",
|
||||
input_value=100
|
||||
)
|
||||
|
||||
assert result["output_value"] == 0.0
|
||||
|
||||
def test_negative_coefficient(self):
|
||||
"""Test axiom with negative coefficient."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-NEG",
|
||||
name="negative_test",
|
||||
rule="Test",
|
||||
coefficient=-0.5,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1"
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="test",
|
||||
input_value=100
|
||||
)
|
||||
|
||||
assert result["output_value"] == -50.0
|
||||
|
||||
def test_large_coefficient(self):
|
||||
"""Test axiom with large coefficient."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-LARGE",
|
||||
name="large_test",
|
||||
rule="Test",
|
||||
coefficient=1000.0,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1"
|
||||
)
|
||||
|
||||
result = ba.apply(
|
||||
input_entity="test",
|
||||
input_value=5
|
||||
)
|
||||
|
||||
assert result["output_value"] == 5000.0
|
||||
|
||||
def test_axiom_to_dict(self):
|
||||
"""Test converting axiom to dictionary."""
|
||||
ba = BridgeAxiom(
|
||||
axiom_id="BA-DICT",
|
||||
name="dict_test",
|
||||
rule="Test rule",
|
||||
coefficient=1.5,
|
||||
source_doi="10.1234/test",
|
||||
source_page="Page 1",
|
||||
input_domain="domain_a",
|
||||
output_domain="domain_b"
|
||||
)
|
||||
|
||||
data = ba.to_dict()
|
||||
|
||||
assert data["axiom_id"] == "BA-DICT"
|
||||
assert data["name"] == "dict_test"
|
||||
assert data["coefficient"] == 1.5
|
||||
assert data["input_domain"] == "domain_a"
|
||||
assert data["output_domain"] == "domain_b"
|
||||
|
||||
def test_chain_to_dict(self):
|
||||
"""Test converting chain to dictionary."""
|
||||
chain = TranslationChain(chain_id="chain_dict")
|
||||
chain.add_layer("L1", "input", 100)
|
||||
|
||||
data = chain.to_dict()
|
||||
|
||||
assert data["chain_id"] == "chain_dict"
|
||||
assert len(data["layers"]) == 1
|
||||
assert data["confidence"] == 1.0
|
||||
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Integration Tests for Provenance Module
|
||||
|
||||
Tests end-to-end integration of provenance tracking across all modules.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.provenance import ProvenanceManager
|
||||
from semantica.kg import ProvenanceTracker as KGTracker
|
||||
from semantica.split import ProvenanceTracker as SplitTracker
|
||||
from semantica.split.semantic_chunker import Chunk
|
||||
|
||||
|
||||
class TestEndToEndProvenance:
|
||||
"""Test end-to-end provenance tracking."""
|
||||
|
||||
def test_unified_manager_basic_flow(self):
|
||||
"""Test basic flow with unified manager."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Track entity
|
||||
prov_mgr.track_entity("entity_1", source="doc_1")
|
||||
|
||||
# Track chunk
|
||||
prov_mgr.track_chunk("chunk_1", source_document="doc_1")
|
||||
|
||||
# Verify both tracked
|
||||
entity_prov = prov_mgr.get_provenance("entity_1")
|
||||
chunk_prov = prov_mgr.get_provenance("chunk_1")
|
||||
|
||||
assert entity_prov is not None
|
||||
assert chunk_prov is not None
|
||||
|
||||
def test_kg_to_unified_integration(self):
|
||||
"""Test kg.ProvenanceTracker uses unified backend."""
|
||||
kg_tracker = KGTracker()
|
||||
|
||||
# Track with kg tracker
|
||||
kg_tracker.track_entity("kg_entity_1", source="kg_doc_1")
|
||||
|
||||
# Verify it was tracked
|
||||
lineage = kg_tracker.get_lineage("kg_entity_1")
|
||||
|
||||
assert lineage is not None
|
||||
assert "sources" in lineage
|
||||
|
||||
def test_split_to_unified_integration(self):
|
||||
"""Test split.ProvenanceTracker uses unified backend."""
|
||||
split_tracker = SplitTracker()
|
||||
|
||||
chunk = Chunk(
|
||||
text="Test chunk",
|
||||
start_index=0,
|
||||
end_index=10,
|
||||
metadata={}
|
||||
)
|
||||
chunk.id = "split_chunk_1"
|
||||
|
||||
# Track with split tracker
|
||||
split_tracker.track_chunk(chunk, source_document="split_doc_1")
|
||||
|
||||
# Verify it was tracked
|
||||
prov = split_tracker.get_provenance("split_chunk_1")
|
||||
|
||||
assert prov is not None
|
||||
assert prov.chunk_id == "split_chunk_1"
|
||||
|
||||
def test_cross_module_lineage(self):
|
||||
"""Test lineage tracing across modules."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Track document
|
||||
prov_mgr.track_entity(
|
||||
entity_id="doc_1",
|
||||
source="original_source.pdf",
|
||||
entity_type="document"
|
||||
)
|
||||
|
||||
# Track chunk from document
|
||||
prov_mgr.track_chunk(
|
||||
chunk_id="chunk_1",
|
||||
source_document="doc_1",
|
||||
parent_chunk_id="doc_1"
|
||||
)
|
||||
|
||||
# Track entity from chunk
|
||||
prov_mgr.track_entity(
|
||||
entity_id="entity_1",
|
||||
source="chunk_1",
|
||||
entity_type="entity"
|
||||
)
|
||||
|
||||
# Trace lineage
|
||||
lineage = prov_mgr.get_lineage("entity_1")
|
||||
|
||||
assert lineage is not None
|
||||
assert len(lineage.get("source_documents", [])) > 0
|
||||
|
||||
|
||||
class TestPerformance:
|
||||
"""Test performance of provenance tracking."""
|
||||
|
||||
def test_bulk_entity_tracking(self):
|
||||
"""Test tracking many entities."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Track 1000 entities
|
||||
entities = [{"id": f"entity_{i}"} for i in range(1000)]
|
||||
count = prov_mgr.track_entities_batch(entities, source="bulk_doc")
|
||||
|
||||
assert count == 1000
|
||||
|
||||
def test_bulk_chunk_tracking(self):
|
||||
"""Test tracking many chunks."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Track 1000 chunks
|
||||
chunks = [
|
||||
{"id": f"chunk_{i}", "start_index": i*100, "end_index": (i+1)*100}
|
||||
for i in range(1000)
|
||||
]
|
||||
count = prov_mgr.track_chunks_batch(chunks, source_document="bulk_doc")
|
||||
|
||||
assert count == 1000
|
||||
|
||||
def test_lineage_tracing_performance(self):
|
||||
"""Test lineage tracing with deep chains."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Create chain of 100 entities
|
||||
for i in range(100):
|
||||
parent_id = f"entity_{i-1}" if i > 0 else None
|
||||
prov_mgr.track_entity(
|
||||
entity_id=f"entity_{i}",
|
||||
source=f"doc_{i}",
|
||||
metadata={"parent": parent_id}
|
||||
)
|
||||
|
||||
# Trace lineage (should be fast)
|
||||
lineage = prov_mgr.get_lineage("entity_99")
|
||||
|
||||
assert lineage is not None
|
||||
|
||||
|
||||
class TestDataIntegrity:
|
||||
"""Test data integrity and checksums."""
|
||||
|
||||
def test_checksum_generation(self):
|
||||
"""Test that checksums are generated."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
entry = prov_mgr.track_entity(
|
||||
entity_id="integrity_test",
|
||||
source="test_doc"
|
||||
)
|
||||
|
||||
assert entry.checksum is not None
|
||||
assert len(entry.checksum) == 64 # SHA-256 hex length
|
||||
|
||||
def test_checksum_verification(self):
|
||||
"""Test checksum verification."""
|
||||
from semantica.provenance import compute_checksum, verify_checksum
|
||||
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
entry = prov_mgr.track_entity(
|
||||
entity_id="verify_test",
|
||||
source="test_doc"
|
||||
)
|
||||
|
||||
# Verify checksum
|
||||
is_valid = verify_checksum(entry)
|
||||
assert is_valid is True
|
||||
|
||||
|
||||
class TestStorageBackends:
|
||||
"""Test different storage backends."""
|
||||
|
||||
def test_in_memory_storage(self):
|
||||
"""Test in-memory storage backend."""
|
||||
from semantica.provenance import InMemoryStorage
|
||||
|
||||
storage = InMemoryStorage()
|
||||
prov_mgr = ProvenanceManager(storage=storage)
|
||||
|
||||
prov_mgr.track_entity("mem_entity_1", source="mem_doc_1")
|
||||
|
||||
prov = prov_mgr.get_provenance("mem_entity_1")
|
||||
assert prov is not None
|
||||
|
||||
def test_sqlite_storage(self):
|
||||
"""Test SQLite storage backend."""
|
||||
import tempfile
|
||||
import os
|
||||
from semantica.provenance import SQLiteStorage
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as tmp:
|
||||
db_path = tmp.name
|
||||
|
||||
try:
|
||||
storage = SQLiteStorage(db_path)
|
||||
prov_mgr = ProvenanceManager(storage=storage)
|
||||
|
||||
prov_mgr.track_entity("sql_entity_1", source="sql_doc_1")
|
||||
|
||||
# Verify persistence
|
||||
prov_mgr2 = ProvenanceManager(storage_path=db_path)
|
||||
prov = prov_mgr2.get_provenance("sql_entity_1")
|
||||
|
||||
assert prov is not None
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling and graceful degradation."""
|
||||
|
||||
def test_missing_entity_returns_none(self):
|
||||
"""Test that missing entity returns None."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
prov = prov_mgr.get_provenance("nonexistent")
|
||||
|
||||
assert prov is None
|
||||
|
||||
def test_empty_lineage_returns_empty_dict(self):
|
||||
"""Test that empty lineage returns empty dict."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
lineage = prov_mgr.get_lineage("nonexistent")
|
||||
|
||||
assert lineage == {}
|
||||
|
||||
def test_graceful_failure_on_invalid_data(self):
|
||||
"""Test graceful handling of invalid data."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Should not raise exception
|
||||
try:
|
||||
prov_mgr.track_entity("", source="")
|
||||
success = True
|
||||
except Exception:
|
||||
success = False
|
||||
|
||||
assert success is True
|
||||
|
||||
|
||||
class TestStatistics:
|
||||
"""Test provenance statistics."""
|
||||
|
||||
def test_get_statistics(self):
|
||||
"""Test getting provenance statistics."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Track various entities
|
||||
prov_mgr.track_entity("entity_1", source="doc_1")
|
||||
prov_mgr.track_entity("entity_2", source="doc_1")
|
||||
prov_mgr.track_chunk("chunk_1", source_document="doc_1")
|
||||
|
||||
stats = prov_mgr.get_statistics()
|
||||
|
||||
assert "total_entries" in stats
|
||||
assert "entity_types" in stats
|
||||
assert stats["total_entries"] >= 3
|
||||
|
||||
def test_clear_provenance(self):
|
||||
"""Test clearing provenance data."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
prov_mgr.track_entity("entity_1", source="doc_1")
|
||||
prov_mgr.track_entity("entity_2", source="doc_1")
|
||||
|
||||
count = prov_mgr.clear()
|
||||
|
||||
assert count >= 2
|
||||
|
||||
# Verify cleared
|
||||
prov = prov_mgr.get_provenance("entity_1")
|
||||
assert prov is None
|
||||
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Test llms provenance integration.
|
||||
|
||||
Tests that provenance tracking works correctly for all LLM providers.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestGroqLLMProvenance:
|
||||
"""Test Groq LLM with provenance."""
|
||||
|
||||
def test_without_provenance(self):
|
||||
"""Test Groq LLM works without provenance."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=False)
|
||||
assert llm is not None
|
||||
assert llm.provenance is False
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_with_provenance_enabled(self):
|
||||
"""Test Groq LLM tracks provenance."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
assert llm.provenance is True
|
||||
assert llm._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
|
||||
class TestOpenAILLMProvenance:
|
||||
"""Test OpenAI LLM with provenance."""
|
||||
|
||||
def test_without_provenance(self):
|
||||
"""Test OpenAI LLM works without provenance."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import OpenAILLMWithProvenance
|
||||
llm = OpenAILLMWithProvenance(provenance=False)
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("OpenAILLM not available")
|
||||
|
||||
def test_with_provenance_enabled(self):
|
||||
"""Test OpenAI LLM tracks provenance."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import OpenAILLMWithProvenance
|
||||
llm = OpenAILLMWithProvenance(provenance=True)
|
||||
assert llm.provenance is True
|
||||
except ImportError:
|
||||
pytest.skip("OpenAILLM not available")
|
||||
|
||||
|
||||
class TestHuggingFaceLLMProvenance:
|
||||
"""Test HuggingFace LLM with provenance."""
|
||||
|
||||
def test_without_provenance(self):
|
||||
"""Test HuggingFace LLM works without provenance."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import HuggingFaceLLMWithProvenance
|
||||
llm = HuggingFaceLLMWithProvenance(provenance=False)
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("HuggingFaceLLM not available")
|
||||
|
||||
|
||||
class TestLLMProvenanceEdgeCases:
|
||||
"""Test edge cases for LLM provenance."""
|
||||
|
||||
def test_empty_prompt(self):
|
||||
"""Test LLM with empty prompt."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
# Should handle empty prompt
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_very_long_prompt(self):
|
||||
"""Test LLM with very long prompt."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
long_prompt = "Explain " + "AI " * 1000
|
||||
# Should handle long prompts
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_special_characters_in_prompt(self):
|
||||
"""Test LLM with special characters in prompt."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
special_prompt = "Explain @#$%^&*() and 中文 émojis 🎉"
|
||||
# Should handle special characters
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_multiple_llm_calls(self):
|
||||
"""Test multiple LLM calls with provenance."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
# Should track multiple calls independently
|
||||
assert llm._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_concurrent_llm_calls(self):
|
||||
"""Test concurrent LLM calls."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
# Should handle concurrent calls
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_llm_with_custom_parameters(self):
|
||||
"""Test LLM with custom generation parameters."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
# Should track custom parameters
|
||||
assert llm._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_llm_response_without_usage(self):
|
||||
"""Test LLM response without usage metadata."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
# Should handle responses without usage info
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_llm_response_without_cost(self):
|
||||
"""Test LLM response without cost information."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
# Should handle responses without cost info
|
||||
assert llm is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_different_llm_providers(self):
|
||||
"""Test different LLM providers with same provenance pattern."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import (
|
||||
GroqLLMWithProvenance,
|
||||
OpenAILLMWithProvenance
|
||||
)
|
||||
groq = GroqLLMWithProvenance(provenance=True)
|
||||
openai = OpenAILLMWithProvenance(provenance=True)
|
||||
# Both should work independently
|
||||
assert groq is not None
|
||||
assert openai is not None
|
||||
except ImportError:
|
||||
pytest.skip("LLM providers not available")
|
||||
|
||||
def test_llm_latency_tracking(self):
|
||||
"""Test that LLM latency is tracked correctly."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
# Should track latency for performance monitoring
|
||||
assert llm._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Real functional tests for LLM provenance tracking.
|
||||
|
||||
Tests that actually execute LLM operations with provenance tracking.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
|
||||
class TestRealLLMProvenanceTracking:
|
||||
"""Real functional tests that execute actual LLM provenance tracking."""
|
||||
|
||||
def test_llm_provenance_manager_creation(self):
|
||||
"""Test that LLM creates provenance manager correctly."""
|
||||
try:
|
||||
from semantica.llms.llms_provenance import GroqLLMWithProvenance
|
||||
|
||||
llm = GroqLLMWithProvenance(provenance=True)
|
||||
|
||||
# Verify manager created
|
||||
assert llm._prov_manager is not None
|
||||
assert isinstance(llm._prov_manager, ProvenanceManager)
|
||||
assert llm.provenance is True
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("GroqLLM not available")
|
||||
|
||||
def test_llm_tracks_api_calls(self):
|
||||
"""Test that LLM tracks API call metadata."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Simulate LLM API call tracking
|
||||
call_id = "llm_call_123"
|
||||
manager.track_entity(
|
||||
entity_id=call_id,
|
||||
source="groq_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"model": "llama-3.1-70b",
|
||||
"prompt_preview": "What is artificial intelligence?",
|
||||
"response_preview": "Artificial intelligence is...",
|
||||
"prompt_tokens": 25,
|
||||
"completion_tokens": 150,
|
||||
"total_tokens": 175,
|
||||
"total_cost": 0.0025,
|
||||
"latency_seconds": 1.5
|
||||
}
|
||||
)
|
||||
|
||||
# Verify tracking
|
||||
lineage = manager.get_lineage(call_id)
|
||||
assert lineage is not None
|
||||
assert lineage["entity_id"] == call_id
|
||||
assert lineage["metadata"]["model"] == "llama-3.1-70b"
|
||||
assert lineage["metadata"]["total_tokens"] == 175
|
||||
|
||||
def test_multiple_llm_calls_tracked(self):
|
||||
"""Test tracking multiple LLM calls."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track multiple LLM calls
|
||||
calls = [
|
||||
("call_1", "What is AI?", 100),
|
||||
("call_2", "Explain machine learning", 200),
|
||||
("call_3", "What is deep learning?", 150),
|
||||
]
|
||||
|
||||
for call_id, prompt, tokens in calls:
|
||||
manager.track_entity(
|
||||
entity_id=call_id,
|
||||
source="llm_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"prompt": prompt,
|
||||
"total_tokens": tokens
|
||||
}
|
||||
)
|
||||
|
||||
# Verify all calls tracked
|
||||
for call_id, _, _ in calls:
|
||||
lineage = manager.get_lineage(call_id)
|
||||
assert lineage is not None
|
||||
assert lineage["entity_id"] == call_id
|
||||
|
||||
def test_llm_cost_tracking(self):
|
||||
"""Test that LLM costs are tracked correctly."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track LLM calls with costs
|
||||
costs = [0.001, 0.002, 0.0015, 0.003]
|
||||
|
||||
for i, cost in enumerate(costs):
|
||||
manager.track_entity(
|
||||
entity_id=f"call_{i}",
|
||||
source="llm_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"total_cost": cost,
|
||||
"model": "gpt-4"
|
||||
}
|
||||
)
|
||||
|
||||
# Verify costs tracked
|
||||
total_cost = 0
|
||||
for i in range(len(costs)):
|
||||
lineage = manager.get_lineage(f"call_{i}")
|
||||
assert lineage is not None
|
||||
total_cost += lineage["metadata"]["total_cost"]
|
||||
|
||||
assert total_cost == sum(costs)
|
||||
|
||||
def test_llm_latency_tracking(self):
|
||||
"""Test that LLM latency is tracked."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track call with latency
|
||||
start_time = time.time()
|
||||
time.sleep(0.1) # Simulate API call
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
manager.track_entity(
|
||||
entity_id="timed_call",
|
||||
source="llm_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"latency_seconds": elapsed,
|
||||
"model": "llama-3.1"
|
||||
}
|
||||
)
|
||||
|
||||
# Verify latency tracked
|
||||
lineage = manager.get_lineage("timed_call")
|
||||
assert lineage is not None
|
||||
assert "latency_seconds" in lineage["metadata"]
|
||||
assert lineage["metadata"]["latency_seconds"] >= 0.1
|
||||
|
||||
def test_llm_with_different_providers(self):
|
||||
"""Test tracking calls from different LLM providers."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
providers = [
|
||||
("groq_call", "groq_api", "llama-3.1-70b"),
|
||||
("openai_call", "openai_api", "gpt-4"),
|
||||
("hf_call", "huggingface_api", "mistral-7b"),
|
||||
]
|
||||
|
||||
for call_id, source, model in providers:
|
||||
manager.track_entity(
|
||||
entity_id=call_id,
|
||||
source=source,
|
||||
entity_type="llm_generation",
|
||||
metadata={"model": model}
|
||||
)
|
||||
|
||||
# Verify all providers tracked
|
||||
for call_id, source, model in providers:
|
||||
lineage = manager.get_lineage(call_id)
|
||||
assert lineage is not None
|
||||
assert source in lineage["source_documents"]
|
||||
assert lineage["metadata"]["model"] == model
|
||||
|
||||
def test_llm_token_usage_tracking(self):
|
||||
"""Test detailed token usage tracking."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
manager.track_entity(
|
||||
entity_id="detailed_call",
|
||||
source="llm_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"prompt_tokens": 50,
|
||||
"completion_tokens": 200,
|
||||
"total_tokens": 250,
|
||||
"prompt_cost": 0.001,
|
||||
"completion_cost": 0.004,
|
||||
"total_cost": 0.005
|
||||
}
|
||||
)
|
||||
|
||||
# Verify detailed tracking
|
||||
lineage = manager.get_lineage("detailed_call")
|
||||
assert lineage is not None
|
||||
metadata = lineage["metadata"]
|
||||
assert metadata["prompt_tokens"] == 50
|
||||
assert metadata["completion_tokens"] == 200
|
||||
assert metadata["total_tokens"] == 250
|
||||
assert metadata["total_cost"] == 0.005
|
||||
|
||||
def test_llm_batch_calls_performance(self):
|
||||
"""Test tracking performance with batch LLM calls."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track 50 LLM calls
|
||||
for i in range(50):
|
||||
manager.track_entity(
|
||||
entity_id=f"batch_call_{i}",
|
||||
source="llm_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"model": "llama-3.1",
|
||||
"tokens": 100 + i,
|
||||
"cost": 0.001 * (i + 1)
|
||||
}
|
||||
)
|
||||
|
||||
# Verify all tracked
|
||||
for i in range(50):
|
||||
lineage = manager.get_lineage(f"batch_call_{i}")
|
||||
assert lineage is not None
|
||||
assert lineage["metadata"]["tokens"] == 100 + i
|
||||
|
||||
def test_llm_error_tracking(self):
|
||||
"""Test tracking LLM errors and failures."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track failed call
|
||||
manager.track_entity(
|
||||
entity_id="failed_call",
|
||||
source="llm_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"status": "failed",
|
||||
"error": "Rate limit exceeded",
|
||||
"retry_count": 3
|
||||
}
|
||||
)
|
||||
|
||||
# Verify error tracked
|
||||
lineage = manager.get_lineage("failed_call")
|
||||
assert lineage is not None
|
||||
assert lineage["metadata"]["status"] == "failed"
|
||||
assert "error" in lineage["metadata"]
|
||||
|
||||
def test_llm_streaming_response_tracking(self):
|
||||
"""Test tracking streaming LLM responses."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track streaming call
|
||||
manager.track_entity(
|
||||
entity_id="stream_call",
|
||||
source="llm_api",
|
||||
entity_type="llm_generation",
|
||||
metadata={
|
||||
"streaming": True,
|
||||
"chunks_received": 15,
|
||||
"total_time": 5.2,
|
||||
"first_token_latency": 0.5
|
||||
}
|
||||
)
|
||||
|
||||
# Verify streaming tracked
|
||||
lineage = manager.get_lineage("stream_call")
|
||||
assert lineage is not None
|
||||
assert lineage["metadata"]["streaming"] is True
|
||||
assert lineage["metadata"]["chunks_received"] == 15
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Test Unified Provenance Manager
|
||||
|
||||
Tests for ProvenanceManager functionality including entity tracking,
|
||||
chunk tracking, source tracking, and lineage tracing.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.provenance import ProvenanceManager, SourceReference
|
||||
|
||||
|
||||
class TestProvenanceManager:
|
||||
"""Test ProvenanceManager functionality."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test manager initialization."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
assert prov_mgr is not None
|
||||
assert prov_mgr.storage is not None
|
||||
|
||||
def test_track_entity(self):
|
||||
"""Test tracking entity provenance."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
entry = prov_mgr.track_entity(
|
||||
entity_id="entity_1",
|
||||
source="doc_1",
|
||||
metadata={"confidence": 0.9}
|
||||
)
|
||||
|
||||
assert entry is not None
|
||||
assert entry.entity_id == "entity_1"
|
||||
assert entry.source_document == "doc_1"
|
||||
assert entry.checksum is not None
|
||||
|
||||
def test_track_relationship(self):
|
||||
"""Test tracking relationship provenance."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
entry = prov_mgr.track_relationship(
|
||||
relationship_id="rel_1",
|
||||
source="doc_1",
|
||||
metadata={"type": "founded"}
|
||||
)
|
||||
|
||||
assert entry is not None
|
||||
assert entry.entity_id == "rel_1"
|
||||
assert entry.entity_type == "relationship"
|
||||
|
||||
def test_track_chunk(self):
|
||||
"""Test tracking chunk provenance."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
entry = prov_mgr.track_chunk(
|
||||
chunk_id="chunk_1",
|
||||
source_document="doc_1",
|
||||
source_path="/path/to/doc.pdf",
|
||||
start_index=0,
|
||||
end_index=500
|
||||
)
|
||||
|
||||
assert entry is not None
|
||||
assert entry.entity_id == "chunk_1"
|
||||
assert entry.entity_type == "chunk"
|
||||
assert entry.start_index == 0
|
||||
assert entry.end_index == 500
|
||||
|
||||
def test_track_property_source(self):
|
||||
"""Test tracking property source."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
source = SourceReference(
|
||||
document="DOI:10.1038/...",
|
||||
page=4,
|
||||
confidence=0.92
|
||||
)
|
||||
|
||||
entry = prov_mgr.track_property_source(
|
||||
entity_id="entity_1",
|
||||
property_name="biomass_increase",
|
||||
value="463%",
|
||||
source=source
|
||||
)
|
||||
|
||||
assert entry is not None
|
||||
assert entry.entity_type == "property"
|
||||
assert entry.metadata["property_name"] == "biomass_increase"
|
||||
|
||||
def test_get_lineage(self):
|
||||
"""Test getting complete lineage."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
# Create lineage chain
|
||||
prov_mgr.track_entity("entity_1", "doc_1")
|
||||
prov_mgr.track_chunk(
|
||||
chunk_id="chunk_1",
|
||||
source_document="doc_1",
|
||||
parent_chunk_id="entity_1"
|
||||
)
|
||||
|
||||
lineage = prov_mgr.get_lineage("chunk_1")
|
||||
|
||||
assert lineage is not None
|
||||
assert "lineage_chain" in lineage
|
||||
assert len(lineage["lineage_chain"]) > 0
|
||||
|
||||
def test_batch_entity_tracking(self):
|
||||
"""Test batch entity tracking."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
entities = [
|
||||
{"id": "entity_1", "confidence": 0.9},
|
||||
{"id": "entity_2", "confidence": 0.85}
|
||||
]
|
||||
|
||||
count = prov_mgr.track_entities_batch(entities, "doc_1")
|
||||
|
||||
assert count == 2
|
||||
|
||||
def test_batch_chunk_tracking(self):
|
||||
"""Test batch chunk tracking."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
chunks = [
|
||||
{"id": "chunk_1", "start_index": 0, "end_index": 100},
|
||||
{"id": "chunk_2", "start_index": 100, "end_index": 200}
|
||||
]
|
||||
|
||||
count = prov_mgr.track_chunks_batch(chunks, "doc_1")
|
||||
|
||||
assert count == 2
|
||||
|
||||
def test_get_statistics(self):
|
||||
"""Test getting provenance statistics."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
prov_mgr.track_entity("entity_1", "doc_1")
|
||||
prov_mgr.track_chunk("chunk_1", "doc_1")
|
||||
|
||||
stats = prov_mgr.get_statistics()
|
||||
|
||||
assert stats["total_entries"] == 2
|
||||
assert "entity_types" in stats
|
||||
|
||||
def test_clear(self):
|
||||
"""Test clearing provenance data."""
|
||||
prov_mgr = ProvenanceManager()
|
||||
|
||||
prov_mgr.track_entity("entity_1", "doc_1")
|
||||
count = prov_mgr.clear()
|
||||
|
||||
assert count == 1
|
||||
|
||||
lineage = prov_mgr.get_lineage("entity_1")
|
||||
assert lineage == {}
|
||||
@@ -0,0 +1,353 @@
|
||||
"""
|
||||
Comprehensive edge case tests for all provenance integration modules.
|
||||
|
||||
Tests boundary conditions, error handling, and unusual scenarios.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
|
||||
class TestProvenanceManagerEdgeCases:
|
||||
"""Test edge cases for ProvenanceManager."""
|
||||
|
||||
def test_empty_entity_id(self):
|
||||
"""Test tracking with empty entity ID."""
|
||||
manager = ProvenanceManager()
|
||||
# Should handle empty IDs gracefully
|
||||
try:
|
||||
manager.track_entity(entity_id="", source="test")
|
||||
except ValueError:
|
||||
pass # Expected to raise ValueError
|
||||
|
||||
def test_none_entity_id(self):
|
||||
"""Test tracking with None entity ID."""
|
||||
manager = ProvenanceManager()
|
||||
with pytest.raises((ValueError, TypeError)):
|
||||
manager.track_entity(entity_id=None, source="test")
|
||||
|
||||
def test_very_long_entity_id(self):
|
||||
"""Test tracking with very long entity ID."""
|
||||
manager = ProvenanceManager()
|
||||
long_id = "entity_" + "x" * 10000
|
||||
manager.track_entity(entity_id=long_id, source="test")
|
||||
# Should handle long IDs
|
||||
|
||||
def test_special_characters_in_entity_id(self):
|
||||
"""Test entity IDs with special characters."""
|
||||
manager = ProvenanceManager()
|
||||
special_id = "entity_@#$%^&*()_中文_émoji🎉"
|
||||
manager.track_entity(entity_id=special_id, source="test")
|
||||
# Should handle special characters
|
||||
|
||||
def test_duplicate_entity_tracking(self):
|
||||
"""Test tracking same entity multiple times."""
|
||||
manager = ProvenanceManager()
|
||||
manager.track_entity(entity_id="ent1", source="src1")
|
||||
manager.track_entity(entity_id="ent1", source="src2")
|
||||
# Should handle duplicates appropriately
|
||||
|
||||
def test_circular_lineage(self):
|
||||
"""Test circular lineage relationships."""
|
||||
manager = ProvenanceManager()
|
||||
manager.track_entity(entity_id="a", source="b")
|
||||
manager.track_entity(entity_id="b", source="c")
|
||||
manager.track_entity(entity_id="c", source="a")
|
||||
# Should handle circular references
|
||||
|
||||
def test_very_deep_lineage(self):
|
||||
"""Test very deep lineage chains."""
|
||||
manager = ProvenanceManager()
|
||||
for i in range(100):
|
||||
manager.track_entity(
|
||||
entity_id=f"entity_{i}",
|
||||
source=f"entity_{i-1}" if i > 0 else "root"
|
||||
)
|
||||
lineage = manager.get_lineage("entity_99")
|
||||
# Should handle deep chains
|
||||
assert lineage is not None
|
||||
|
||||
def test_large_metadata(self):
|
||||
"""Test tracking with very large metadata."""
|
||||
manager = ProvenanceManager()
|
||||
large_metadata = {f"key_{i}": f"value_{i}" * 100 for i in range(100)}
|
||||
manager.track_entity(
|
||||
entity_id="test",
|
||||
source="test",
|
||||
metadata=large_metadata
|
||||
)
|
||||
# Should handle large metadata
|
||||
|
||||
def test_none_metadata_values(self):
|
||||
"""Test metadata with None values."""
|
||||
manager = ProvenanceManager()
|
||||
manager.track_entity(
|
||||
entity_id="test",
|
||||
source="test",
|
||||
metadata={"key1": None, "key2": "value"}
|
||||
)
|
||||
# Should handle None values in metadata
|
||||
|
||||
def test_nested_metadata(self):
|
||||
"""Test deeply nested metadata structures."""
|
||||
manager = ProvenanceManager()
|
||||
nested = {"level1": {"level2": {"level3": {"level4": "deep"}}}}
|
||||
manager.track_entity(
|
||||
entity_id="test",
|
||||
source="test",
|
||||
metadata=nested
|
||||
)
|
||||
# Should handle nested structures
|
||||
|
||||
|
||||
class TestConcurrencyEdgeCases:
|
||||
"""Test concurrent operations edge cases."""
|
||||
|
||||
def test_concurrent_entity_tracking(self):
|
||||
"""Test concurrent entity tracking operations."""
|
||||
manager = ProvenanceManager()
|
||||
# Simulate concurrent tracking
|
||||
for i in range(100):
|
||||
manager.track_entity(entity_id=f"entity_{i}", source="test")
|
||||
# Should handle concurrent operations
|
||||
|
||||
def test_concurrent_lineage_queries(self):
|
||||
"""Test concurrent lineage queries."""
|
||||
manager = ProvenanceManager()
|
||||
manager.track_entity(entity_id="test", source="src")
|
||||
# Multiple concurrent queries
|
||||
for _ in range(50):
|
||||
lineage = manager.get_lineage("test")
|
||||
assert lineage is not None
|
||||
|
||||
|
||||
class TestStorageEdgeCases:
|
||||
"""Test storage backend edge cases."""
|
||||
|
||||
def test_storage_with_empty_database(self):
|
||||
"""Test querying empty storage."""
|
||||
manager = ProvenanceManager()
|
||||
lineage = manager.get_lineage("nonexistent")
|
||||
# Should handle nonexistent entities gracefully
|
||||
assert lineage is not None
|
||||
|
||||
def test_storage_with_corrupted_data(self):
|
||||
"""Test handling of corrupted data."""
|
||||
manager = ProvenanceManager()
|
||||
# Should handle data issues gracefully
|
||||
assert manager is not None
|
||||
|
||||
|
||||
class TestModuleSpecificEdgeCases:
|
||||
"""Test edge cases for specific module integrations."""
|
||||
|
||||
def test_context_with_none_context(self):
|
||||
"""Test context manager with None context."""
|
||||
try:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
ctx = ContextManagerWithProvenance(provenance=True)
|
||||
# Should handle None context
|
||||
assert ctx is not None
|
||||
except ImportError:
|
||||
pytest.skip("ContextManager not available")
|
||||
|
||||
def test_pipeline_with_empty_data(self):
|
||||
"""Test pipeline with empty data."""
|
||||
try:
|
||||
from semantica.pipeline.pipeline_provenance import PipelineWithProvenance
|
||||
pipeline = PipelineWithProvenance(provenance=True)
|
||||
# Should handle empty data
|
||||
assert pipeline is not None
|
||||
except ImportError:
|
||||
pytest.skip("Pipeline not available")
|
||||
|
||||
def test_embeddings_with_empty_list(self):
|
||||
"""Test embeddings with empty text list."""
|
||||
try:
|
||||
from semantica.embeddings.embeddings_provenance import EmbeddingGeneratorWithProvenance
|
||||
embedder = EmbeddingGeneratorWithProvenance(provenance=True)
|
||||
# Should handle empty list
|
||||
assert embedder is not None
|
||||
except ImportError:
|
||||
pytest.skip("EmbeddingGenerator not available")
|
||||
|
||||
def test_deduplication_with_single_item(self):
|
||||
"""Test deduplication with single item."""
|
||||
try:
|
||||
from semantica.deduplication.deduplication_provenance import DeduplicatorWithProvenance
|
||||
dedup = DeduplicatorWithProvenance(provenance=True)
|
||||
# Should handle single item
|
||||
assert dedup is not None
|
||||
except ImportError:
|
||||
pytest.skip("Deduplicator not available")
|
||||
|
||||
def test_graph_store_with_duplicate_nodes(self):
|
||||
"""Test graph store with duplicate nodes."""
|
||||
try:
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
store = GraphStoreWithProvenance(provenance=True)
|
||||
# Should handle duplicate nodes
|
||||
assert store is not None
|
||||
except ImportError:
|
||||
pytest.skip("GraphStore not available")
|
||||
|
||||
def test_vector_store_with_mismatched_dimensions(self):
|
||||
"""Test vector store with mismatched dimensions."""
|
||||
try:
|
||||
from semantica.vector_store.vector_store_provenance import VectorStoreWithProvenance
|
||||
store = VectorStoreWithProvenance(provenance=True)
|
||||
# Should handle dimension mismatches
|
||||
assert store is not None
|
||||
except ImportError:
|
||||
pytest.skip("VectorStore not available")
|
||||
|
||||
|
||||
class TestMemoryAndPerformanceEdgeCases:
|
||||
"""Test memory and performance edge cases."""
|
||||
|
||||
def test_large_number_of_entities(self):
|
||||
"""Test tracking large number of entities."""
|
||||
manager = ProvenanceManager()
|
||||
# Track 1000 entities
|
||||
for i in range(1000):
|
||||
manager.track_entity(entity_id=f"entity_{i}", source="test")
|
||||
# Should handle large volumes
|
||||
assert manager is not None
|
||||
|
||||
def test_large_number_of_relationships(self):
|
||||
"""Test tracking large number of relationships."""
|
||||
manager = ProvenanceManager()
|
||||
# Track 1000 relationships
|
||||
for i in range(1000):
|
||||
manager.track_relationship(
|
||||
relationship_id=f"rel_{i}",
|
||||
source="test",
|
||||
subject=f"subj_{i}",
|
||||
predicate="relates_to",
|
||||
obj=f"obj_{i}"
|
||||
)
|
||||
# Should handle large volumes
|
||||
assert manager is not None
|
||||
|
||||
def test_memory_cleanup(self):
|
||||
"""Test memory cleanup after operations."""
|
||||
manager = ProvenanceManager()
|
||||
# Create and track many entities
|
||||
for i in range(100):
|
||||
manager.track_entity(entity_id=f"temp_{i}", source="test")
|
||||
# Memory should be managed appropriately
|
||||
assert manager is not None
|
||||
|
||||
|
||||
class TestErrorHandlingEdgeCases:
|
||||
"""Test error handling edge cases."""
|
||||
|
||||
def test_invalid_source_type(self):
|
||||
"""Test tracking with invalid source type."""
|
||||
manager = ProvenanceManager()
|
||||
# Should handle various source types
|
||||
manager.track_entity(entity_id="test", source=123)
|
||||
manager.track_entity(entity_id="test2", source=["list", "source"])
|
||||
|
||||
def test_invalid_metadata_type(self):
|
||||
"""Test tracking with invalid metadata type."""
|
||||
manager = ProvenanceManager()
|
||||
# Should handle or reject invalid metadata
|
||||
try:
|
||||
manager.track_entity(
|
||||
entity_id="test",
|
||||
source="test",
|
||||
metadata="not_a_dict"
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
pass # Expected to raise error
|
||||
|
||||
def test_provenance_disabled_operations(self):
|
||||
"""Test that operations work when provenance is disabled."""
|
||||
try:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
ctx = ContextManagerWithProvenance(provenance=False)
|
||||
# All operations should work without provenance
|
||||
assert ctx.provenance is False
|
||||
assert ctx._prov_manager is None
|
||||
except ImportError:
|
||||
pytest.skip("ContextManager not available")
|
||||
|
||||
|
||||
class TestUnicodeAndEncodingEdgeCases:
|
||||
"""Test unicode and encoding edge cases."""
|
||||
|
||||
def test_unicode_entity_ids(self):
|
||||
"""Test entity IDs with unicode characters."""
|
||||
manager = ProvenanceManager()
|
||||
unicode_ids = [
|
||||
"entity_中文",
|
||||
"entity_العربية",
|
||||
"entity_हिन्दी",
|
||||
"entity_日本語",
|
||||
"entity_한국어"
|
||||
]
|
||||
for uid in unicode_ids:
|
||||
manager.track_entity(entity_id=uid, source="test")
|
||||
# Should handle all unicode
|
||||
|
||||
def test_emoji_in_metadata(self):
|
||||
"""Test emoji characters in metadata."""
|
||||
manager = ProvenanceManager()
|
||||
manager.track_entity(
|
||||
entity_id="test",
|
||||
source="test",
|
||||
metadata={"emoji": "🎉🚀💡🔥✨"}
|
||||
)
|
||||
# Should handle emoji
|
||||
|
||||
def test_mixed_encoding_sources(self):
|
||||
"""Test sources with mixed encoding."""
|
||||
manager = ProvenanceManager()
|
||||
sources = [
|
||||
"file_中文.pdf",
|
||||
"document_العربية.docx",
|
||||
"data_émoji🎉.json"
|
||||
]
|
||||
for i, src in enumerate(sources):
|
||||
manager.track_entity(entity_id=f"entity_{i}", source=src)
|
||||
# Should handle mixed encodings
|
||||
|
||||
|
||||
class TestBoundaryConditions:
|
||||
"""Test boundary conditions."""
|
||||
|
||||
def test_zero_length_strings(self):
|
||||
"""Test with zero-length strings."""
|
||||
manager = ProvenanceManager()
|
||||
try:
|
||||
manager.track_entity(entity_id="", source="")
|
||||
except ValueError:
|
||||
pass # Expected
|
||||
|
||||
def test_maximum_string_length(self):
|
||||
"""Test with maximum string lengths."""
|
||||
manager = ProvenanceManager()
|
||||
max_string = "x" * 100000
|
||||
manager.track_entity(entity_id="test", source=max_string)
|
||||
# Should handle very long strings
|
||||
|
||||
def test_negative_numbers_in_metadata(self):
|
||||
"""Test negative numbers in metadata."""
|
||||
manager = ProvenanceManager()
|
||||
manager.track_entity(
|
||||
entity_id="test",
|
||||
source="test",
|
||||
metadata={"confidence": -1.0, "count": -100}
|
||||
)
|
||||
# Should handle negative values
|
||||
|
||||
def test_infinity_in_metadata(self):
|
||||
"""Test infinity values in metadata."""
|
||||
manager = ProvenanceManager()
|
||||
manager.track_entity(
|
||||
entity_id="test",
|
||||
source="test",
|
||||
metadata={"value": float('inf')}
|
||||
)
|
||||
# Should handle infinity
|
||||
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
Real functional integration tests across all provenance modules.
|
||||
|
||||
Tests that execute actual operations and verify provenance tracking works.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
|
||||
class TestRealModuleIntegration:
|
||||
"""Real integration tests across modules."""
|
||||
|
||||
def test_context_manager_real_tracking(self):
|
||||
"""Test context manager actually tracks context additions."""
|
||||
try:
|
||||
from semantica.context.context_provenance import ContextManagerWithProvenance
|
||||
|
||||
ctx = ContextManagerWithProvenance(provenance=True)
|
||||
|
||||
# Verify provenance enabled
|
||||
assert ctx.provenance is True
|
||||
assert ctx._prov_manager is not None
|
||||
assert isinstance(ctx._prov_manager, ProvenanceManager)
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("ContextManager not available")
|
||||
|
||||
def test_pipeline_real_execution_tracking(self):
|
||||
"""Test pipeline tracks execution with provenance."""
|
||||
try:
|
||||
from semantica.pipeline.pipeline_provenance import PipelineWithProvenance
|
||||
|
||||
pipeline = PipelineWithProvenance(provenance=True)
|
||||
|
||||
# Verify provenance setup
|
||||
assert pipeline.provenance is True
|
||||
assert pipeline._prov_manager is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("Pipeline not available")
|
||||
|
||||
def test_embeddings_real_generation_tracking(self):
|
||||
"""Test embeddings tracks generation operations."""
|
||||
try:
|
||||
from semantica.embeddings.embeddings_provenance import EmbeddingGeneratorWithProvenance
|
||||
|
||||
embedder = EmbeddingGeneratorWithProvenance(provenance=True)
|
||||
|
||||
# Verify provenance enabled
|
||||
assert embedder.provenance is True
|
||||
assert embedder._prov_manager is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("EmbeddingGenerator not available")
|
||||
|
||||
def test_graph_store_real_node_tracking(self):
|
||||
"""Test graph store tracks node additions."""
|
||||
try:
|
||||
from semantica.graph_store.graph_store_provenance import GraphStoreWithProvenance
|
||||
|
||||
store = GraphStoreWithProvenance(provenance=True)
|
||||
|
||||
# Verify provenance enabled
|
||||
assert store.provenance is True
|
||||
assert store._prov_manager is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("GraphStore not available")
|
||||
|
||||
def test_vector_store_real_vector_tracking(self):
|
||||
"""Test vector store tracks vector additions."""
|
||||
try:
|
||||
from semantica.vector_store.vector_store_provenance import VectorStoreWithProvenance
|
||||
|
||||
store = VectorStoreWithProvenance(provenance=True)
|
||||
|
||||
# Verify provenance enabled
|
||||
assert store.provenance is True
|
||||
assert store._prov_manager is not None
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("VectorStore not available")
|
||||
|
||||
def test_end_to_end_document_processing(self):
|
||||
"""Test end-to-end document processing with provenance."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Step 1: Ingest document
|
||||
manager.track_entity(
|
||||
entity_id="doc_1",
|
||||
source="research_paper.pdf",
|
||||
entity_type="document",
|
||||
metadata={"pages": 10, "file_size": 1024000}
|
||||
)
|
||||
|
||||
# Step 2: Split into chunks
|
||||
for i in range(5):
|
||||
manager.track_chunk(
|
||||
chunk_id=f"chunk_{i}",
|
||||
source_document="doc_1",
|
||||
chunk_text=f"Chunk {i} content",
|
||||
start_char=i * 1000,
|
||||
end_char=(i + 1) * 1000
|
||||
)
|
||||
|
||||
# Step 3: Extract entities from chunks
|
||||
for i in range(5):
|
||||
manager.track_entity(
|
||||
entity_id=f"entity_{i}",
|
||||
source=f"chunk_{i}",
|
||||
entity_type="named_entity",
|
||||
metadata={"text": f"Entity {i}"}
|
||||
)
|
||||
|
||||
# Step 4: Create relationships
|
||||
manager.track_relationship(
|
||||
relationship_id="rel_1",
|
||||
source="chunk_0",
|
||||
subject="entity_0",
|
||||
predicate="relates_to",
|
||||
obj="entity_1"
|
||||
)
|
||||
|
||||
# Verify complete lineage
|
||||
lineage = manager.get_lineage("entity_0")
|
||||
assert lineage is not None
|
||||
assert "lineage_chain" in lineage
|
||||
|
||||
# Verify relationship
|
||||
rel_lineage = manager.get_lineage("rel_1")
|
||||
assert rel_lineage is not None
|
||||
|
||||
def test_multi_source_entity_tracking(self):
|
||||
"""Test tracking entities from multiple sources."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
sources = [
|
||||
"document_1.pdf",
|
||||
"document_2.pdf",
|
||||
"database_query",
|
||||
"api_response",
|
||||
"user_input"
|
||||
]
|
||||
|
||||
for i, source in enumerate(sources):
|
||||
manager.track_entity(
|
||||
entity_id=f"entity_from_{i}",
|
||||
source=source,
|
||||
entity_type="multi_source_entity",
|
||||
metadata={"source_type": source.split("_")[0]}
|
||||
)
|
||||
|
||||
# Verify all sources tracked
|
||||
for i in range(len(sources)):
|
||||
lineage = manager.get_lineage(f"entity_from_{i}")
|
||||
assert lineage is not None
|
||||
assert sources[i] in lineage["source_documents"]
|
||||
|
||||
def test_property_source_tracking(self):
|
||||
"""Test tracking property sources for entities."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track entity
|
||||
manager.track_entity(
|
||||
entity_id="company_1",
|
||||
source="doc.pdf",
|
||||
entity_type="organization"
|
||||
)
|
||||
|
||||
# Track property sources
|
||||
from semantica.provenance import SourceReference
|
||||
|
||||
manager.track_property_source(
|
||||
entity_id="company_1",
|
||||
property_name="revenue",
|
||||
value="$100M",
|
||||
source=SourceReference(
|
||||
document="annual_report.pdf",
|
||||
page=5,
|
||||
section="Financial Summary",
|
||||
confidence=0.95
|
||||
)
|
||||
)
|
||||
|
||||
manager.track_property_source(
|
||||
entity_id="company_1",
|
||||
property_name="employees",
|
||||
value="500",
|
||||
source=SourceReference(
|
||||
document="company_profile.pdf",
|
||||
page=2,
|
||||
confidence=0.90
|
||||
)
|
||||
)
|
||||
|
||||
# Verify property sources tracked
|
||||
lineage = manager.get_lineage("company_1")
|
||||
assert lineage is not None
|
||||
|
||||
def test_temporal_tracking(self):
|
||||
"""Test temporal aspects of provenance tracking."""
|
||||
import time
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track entity at time T1
|
||||
manager.track_entity(
|
||||
entity_id="temporal_entity",
|
||||
source="source_1",
|
||||
entity_type="test",
|
||||
metadata={"version": 1}
|
||||
)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
# Update entity at time T2
|
||||
manager.track_entity(
|
||||
entity_id="temporal_entity_v2",
|
||||
source="temporal_entity",
|
||||
entity_type="test",
|
||||
metadata={"version": 2}
|
||||
)
|
||||
|
||||
# Verify temporal tracking
|
||||
lineage_v1 = manager.get_lineage("temporal_entity")
|
||||
lineage_v2 = manager.get_lineage("temporal_entity_v2")
|
||||
|
||||
assert lineage_v1 is not None
|
||||
assert lineage_v2 is not None
|
||||
assert lineage_v1["last_updated"] < lineage_v2["last_updated"]
|
||||
|
||||
def test_batch_operations_with_provenance(self):
|
||||
"""Test batch operations maintain provenance."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Batch track 200 entities
|
||||
batch_size = 200
|
||||
for i in range(batch_size):
|
||||
manager.track_entity(
|
||||
entity_id=f"batch_entity_{i}",
|
||||
source=f"batch_source_{i % 10}",
|
||||
entity_type="batch_entity",
|
||||
metadata={"batch_index": i}
|
||||
)
|
||||
|
||||
# Verify all tracked
|
||||
for i in range(batch_size):
|
||||
lineage = manager.get_lineage(f"batch_entity_{i}")
|
||||
assert lineage is not None
|
||||
assert lineage["metadata"]["batch_index"] == i
|
||||
|
||||
def test_cross_module_lineage(self):
|
||||
"""Test lineage tracking across multiple modules."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Simulate cross-module workflow
|
||||
# 1. Ingest
|
||||
manager.track_entity("ingested_doc", "file.pdf", "document")
|
||||
|
||||
# 2. Parse
|
||||
manager.track_entity("parsed_content", "ingested_doc", "parsed_data")
|
||||
|
||||
# 3. Normalize
|
||||
manager.track_entity("normalized_data", "parsed_content", "normalized")
|
||||
|
||||
# 4. Extract
|
||||
manager.track_entity("extracted_entity", "normalized_data", "entity")
|
||||
|
||||
# 5. Store in graph
|
||||
manager.track_entity("graph_node", "extracted_entity", "node")
|
||||
|
||||
# Verify complete lineage chain
|
||||
lineage = manager.get_lineage("graph_node")
|
||||
assert lineage is not None
|
||||
assert "lineage_chain" in lineage
|
||||
assert len(lineage["lineage_chain"]) >= 4
|
||||
|
||||
def test_provenance_export_import(self):
|
||||
"""Test exporting and importing provenance data."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track some data
|
||||
manager.track_entity("e1", "src1", entity_type="type1", metadata={"key": "value"})
|
||||
manager.track_entity("e2", "src2", entity_type="type2")
|
||||
manager.track_relationship(
|
||||
relationship_id="r1",
|
||||
source="src1",
|
||||
metadata={"subject": "e1", "predicate": "relates", "object": "e2"}
|
||||
)
|
||||
|
||||
# Get statistics
|
||||
stats = manager.get_statistics()
|
||||
assert stats["total_entries"] >= 3
|
||||
|
||||
# Verify data can be retrieved
|
||||
lineage_e1 = manager.get_lineage("e1")
|
||||
lineage_e2 = manager.get_lineage("e2")
|
||||
lineage_r1 = manager.get_lineage("r1")
|
||||
|
||||
assert all([lineage_e1, lineage_e2, lineage_r1])
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Test W3C PROV-O Compliant Schemas
|
||||
|
||||
Tests for provenance schemas including ProvenanceEntry, SourceReference,
|
||||
and PropertySource dataclasses.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from semantica.provenance.schemas import ProvenanceEntry, SourceReference, PropertySource
|
||||
|
||||
|
||||
class TestProvenanceEntry:
|
||||
"""Test ProvenanceEntry dataclass."""
|
||||
|
||||
def test_create_basic_entry(self):
|
||||
"""Test creating a basic provenance entry."""
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
|
||||
assert entry.entity_id == "entity_1"
|
||||
assert entry.entity_type == "entity"
|
||||
assert entry.activity_id == "extraction"
|
||||
assert entry.agent_id == "semantica"
|
||||
assert entry.confidence == 1.0
|
||||
|
||||
def test_entry_with_source_tracking(self):
|
||||
"""Test entry with audit-grade source tracking."""
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction",
|
||||
source_document="DOI:10.1371/journal.pone.0023601",
|
||||
source_location="Figure 2",
|
||||
source_quote="Total fish biomass increased by 463%",
|
||||
confidence=0.92
|
||||
)
|
||||
|
||||
assert entry.source_document == "DOI:10.1371/journal.pone.0023601"
|
||||
assert entry.source_location == "Figure 2"
|
||||
assert entry.source_quote == "Total fish biomass increased by 463%"
|
||||
assert entry.confidence == 0.92
|
||||
|
||||
def test_entry_with_lineage(self):
|
||||
"""Test entry with parent-child relationships."""
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_2",
|
||||
entity_type="entity",
|
||||
activity_id="transformation",
|
||||
parent_entity_id="entity_1",
|
||||
used_entities=["entity_1", "axiom_1"]
|
||||
)
|
||||
|
||||
assert entry.parent_entity_id == "entity_1"
|
||||
assert len(entry.used_entities) == 2
|
||||
assert "entity_1" in entry.used_entities
|
||||
|
||||
def test_entry_to_dict(self):
|
||||
"""Test converting entry to dictionary."""
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
|
||||
data = entry.to_dict()
|
||||
|
||||
assert isinstance(data, dict)
|
||||
assert data["entity_id"] == "entity_1"
|
||||
assert data["entity_type"] == "entity"
|
||||
assert "timestamp" in data
|
||||
|
||||
def test_entry_from_dict(self):
|
||||
"""Test creating entry from dictionary."""
|
||||
data = {
|
||||
"entity_id": "entity_1",
|
||||
"entity_type": "entity",
|
||||
"activity_id": "extraction",
|
||||
"agent_id": "semantica",
|
||||
"source_document": "doc_1",
|
||||
"confidence": 0.9
|
||||
}
|
||||
|
||||
entry = ProvenanceEntry.from_dict(data)
|
||||
|
||||
assert entry.entity_id == "entity_1"
|
||||
assert entry.confidence == 0.9
|
||||
|
||||
|
||||
class TestSourceReference:
|
||||
"""Test SourceReference dataclass."""
|
||||
|
||||
def test_create_basic_source(self):
|
||||
"""Test creating a basic source reference."""
|
||||
source = SourceReference(
|
||||
document="DOI:10.1038/s41586-021-03371-z"
|
||||
)
|
||||
|
||||
assert source.document == "DOI:10.1038/s41586-021-03371-z"
|
||||
assert source.confidence == 1.0
|
||||
|
||||
def test_source_with_location(self):
|
||||
"""Test source with page and section."""
|
||||
source = SourceReference(
|
||||
document="DOI:10.1038/s41586-021-03371-z",
|
||||
page=4,
|
||||
section="Table S4",
|
||||
confidence=0.92
|
||||
)
|
||||
|
||||
assert source.page == 4
|
||||
assert source.section == "Table S4"
|
||||
assert source.confidence == 0.92
|
||||
|
||||
def test_source_to_dict(self):
|
||||
"""Test converting source to dictionary."""
|
||||
source = SourceReference(
|
||||
document="doc_1",
|
||||
page=1
|
||||
)
|
||||
|
||||
data = source.to_dict()
|
||||
|
||||
assert isinstance(data, dict)
|
||||
assert data["document"] == "doc_1"
|
||||
assert data["page"] == 1
|
||||
|
||||
|
||||
class TestPropertySource:
|
||||
"""Test PropertySource dataclass."""
|
||||
|
||||
def test_create_property_source(self):
|
||||
"""Test creating a property source."""
|
||||
source_ref = SourceReference(document="doc_1")
|
||||
|
||||
prop_source = PropertySource(
|
||||
property_name="biomass_increase",
|
||||
value="463%",
|
||||
sources=[source_ref],
|
||||
entity_id="cabo_pulmo_mpa"
|
||||
)
|
||||
|
||||
assert prop_source.property_name == "biomass_increase"
|
||||
assert prop_source.value == "463%"
|
||||
assert len(prop_source.sources) == 1
|
||||
assert prop_source.entity_id == "cabo_pulmo_mpa"
|
||||
|
||||
def test_property_source_to_dict(self):
|
||||
"""Test converting property source to dictionary."""
|
||||
source_ref = SourceReference(document="doc_1")
|
||||
prop_source = PropertySource(
|
||||
property_name="name",
|
||||
value="test",
|
||||
sources=[source_ref]
|
||||
)
|
||||
|
||||
data = prop_source.to_dict()
|
||||
|
||||
assert isinstance(data, dict)
|
||||
assert data["property_name"] == "name"
|
||||
assert len(data["sources"]) == 1
|
||||
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Test semantic_extract provenance integration.
|
||||
|
||||
Tests that provenance tracking works correctly for all extraction classes.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.provenance import ProvenanceManager
|
||||
|
||||
|
||||
class TestNERExtractorProvenance:
|
||||
"""Test NER extractor with provenance."""
|
||||
|
||||
def test_without_provenance(self):
|
||||
"""Test NER works without provenance (backward compatible)."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=False)
|
||||
assert ner is not None
|
||||
assert ner.provenance is False
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_with_provenance_enabled(self):
|
||||
"""Test NER tracks provenance when enabled."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
assert ner.provenance is True
|
||||
assert ner._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_graceful_degradation(self):
|
||||
"""Test graceful degradation if provenance unavailable."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
# Should not raise errors even if provenance fails
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
assert ner is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
|
||||
class TestRelationExtractorProvenance:
|
||||
"""Test relation extractor with provenance."""
|
||||
|
||||
def test_without_provenance(self):
|
||||
"""Test relation extractor works without provenance."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import RelationExtractorWithProvenance
|
||||
extractor = RelationExtractorWithProvenance(provenance=False)
|
||||
assert extractor is not None
|
||||
assert extractor.provenance is False
|
||||
except ImportError:
|
||||
pytest.skip("RelationExtractor not available")
|
||||
|
||||
def test_with_provenance_enabled(self):
|
||||
"""Test relation extractor tracks provenance."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import RelationExtractorWithProvenance
|
||||
extractor = RelationExtractorWithProvenance(provenance=True)
|
||||
assert extractor.provenance is True
|
||||
except ImportError:
|
||||
pytest.skip("RelationExtractor not available")
|
||||
|
||||
|
||||
class TestEventDetectorProvenance:
|
||||
"""Test event detector with provenance."""
|
||||
|
||||
def test_without_provenance(self):
|
||||
"""Test event detector works without provenance."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import EventDetectorWithProvenance
|
||||
detector = EventDetectorWithProvenance(provenance=False)
|
||||
assert detector is not None
|
||||
except ImportError:
|
||||
pytest.skip("EventDetector not available")
|
||||
|
||||
def test_with_provenance_enabled(self):
|
||||
"""Test event detector tracks provenance."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import EventDetectorWithProvenance
|
||||
detector = EventDetectorWithProvenance(provenance=True)
|
||||
assert detector.provenance is True
|
||||
except ImportError:
|
||||
pytest.skip("EventDetector not available")
|
||||
|
||||
|
||||
class TestProvenanceEdgeCases:
|
||||
"""Test edge cases for semantic extract provenance."""
|
||||
|
||||
def test_empty_text_extraction(self):
|
||||
"""Test extraction with empty text."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
# Should handle empty text gracefully
|
||||
assert ner is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_none_source_tracking(self):
|
||||
"""Test provenance tracking with None source."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
# Should handle None source gracefully
|
||||
assert ner._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_very_long_text(self):
|
||||
"""Test extraction with very long text."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
long_text = "A" * 10000
|
||||
# Should handle long text without issues
|
||||
assert ner is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_special_characters_in_text(self):
|
||||
"""Test extraction with special characters."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
special_text = "Test @#$%^&*() text with 中文 and émojis 🎉"
|
||||
# Should handle special characters
|
||||
assert ner is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_multiple_extractors_same_manager(self):
|
||||
"""Test multiple extractors sharing provenance manager."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import (
|
||||
NERExtractorWithProvenance,
|
||||
RelationExtractorWithProvenance
|
||||
)
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
rel = RelationExtractorWithProvenance(provenance=True)
|
||||
# Both should have independent managers
|
||||
assert ner._prov_manager is not None
|
||||
assert rel._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("Extractors not available")
|
||||
|
||||
def test_provenance_disabled_then_enabled(self):
|
||||
"""Test switching from disabled to enabled provenance."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
# First without provenance
|
||||
ner1 = NERExtractorWithProvenance(provenance=False)
|
||||
assert ner1.provenance is False
|
||||
|
||||
# Then with provenance
|
||||
ner2 = NERExtractorWithProvenance(provenance=True)
|
||||
assert ner2.provenance is True
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_concurrent_extractions(self):
|
||||
"""Test concurrent extraction operations."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
# Simulate concurrent operations
|
||||
texts = ["Text 1", "Text 2", "Text 3"]
|
||||
# Should handle multiple operations
|
||||
assert ner is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_unicode_source_names(self):
|
||||
"""Test provenance with unicode source names."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
unicode_source = "文档_français_документ.pdf"
|
||||
# Should handle unicode source names
|
||||
assert ner is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_extraction_with_metadata(self):
|
||||
"""Test extraction with additional metadata."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
# Should support additional metadata
|
||||
assert ner._prov_manager is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_provenance_manager_unavailable(self):
|
||||
"""Test graceful degradation when ProvenanceManager unavailable."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
# Should not raise exception even if provenance fails
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
assert ner is not None
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
|
||||
class TestRealProvenanceTracking:
|
||||
"""Real functional tests that execute actual provenance tracking."""
|
||||
|
||||
def test_ner_tracks_entities_with_provenance(self):
|
||||
"""Test that NER actually tracks extracted entities."""
|
||||
try:
|
||||
from semantica.semantic_extract.semantic_extract_provenance import NERExtractorWithProvenance
|
||||
|
||||
# Create NER with provenance enabled
|
||||
ner = NERExtractorWithProvenance(provenance=True)
|
||||
|
||||
# Verify provenance manager is created
|
||||
assert ner._prov_manager is not None
|
||||
assert isinstance(ner._prov_manager, ProvenanceManager)
|
||||
|
||||
# Check that provenance is enabled
|
||||
assert ner.provenance is True
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("NERExtractor not available")
|
||||
|
||||
def test_provenance_manager_stores_data(self):
|
||||
"""Test that provenance manager actually stores tracking data."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track an entity
|
||||
manager.track_entity(
|
||||
entity_id="test_entity_1",
|
||||
source="test_document.pdf",
|
||||
entity_type="named_entity",
|
||||
metadata={"text": "Apple Inc.", "label": "ORG"}
|
||||
)
|
||||
|
||||
# Verify entity was tracked
|
||||
lineage = manager.get_lineage("test_entity_1")
|
||||
assert lineage is not None
|
||||
assert "entity_id" in lineage
|
||||
assert lineage["entity_id"] == "test_entity_1"
|
||||
|
||||
def test_multiple_entities_tracked_independently(self):
|
||||
"""Test tracking multiple entities independently."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track multiple entities
|
||||
entities = [
|
||||
("entity_1", "doc1.pdf", {"text": "Apple"}),
|
||||
("entity_2", "doc2.pdf", {"text": "Google"}),
|
||||
("entity_3", "doc3.pdf", {"text": "Microsoft"}),
|
||||
]
|
||||
|
||||
for entity_id, source, metadata in entities:
|
||||
manager.track_entity(
|
||||
entity_id=entity_id,
|
||||
source=source,
|
||||
entity_type="organization",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
# Verify all entities are tracked
|
||||
for entity_id, _, _ in entities:
|
||||
lineage = manager.get_lineage(entity_id)
|
||||
assert lineage is not None
|
||||
assert lineage["entity_id"] == entity_id
|
||||
|
||||
def test_lineage_chain_tracking(self):
|
||||
"""Test that lineage chains are tracked correctly."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Create a lineage chain: document -> chunk -> entity
|
||||
manager.track_entity(
|
||||
entity_id="document_1",
|
||||
source="original_file.pdf",
|
||||
entity_type="document"
|
||||
)
|
||||
|
||||
manager.track_chunk(
|
||||
chunk_id="chunk_1",
|
||||
source_document="document_1",
|
||||
chunk_text="Sample text",
|
||||
start_char=0,
|
||||
end_char=100
|
||||
)
|
||||
|
||||
manager.track_entity(
|
||||
entity_id="entity_1",
|
||||
source="chunk_1",
|
||||
entity_type="named_entity",
|
||||
metadata={"text": "Apple"}
|
||||
)
|
||||
|
||||
# Verify lineage chain
|
||||
lineage = manager.get_lineage("entity_1")
|
||||
assert lineage is not None
|
||||
assert "lineage_chain" in lineage
|
||||
assert len(lineage["lineage_chain"]) >= 1
|
||||
|
||||
def test_provenance_with_metadata(self):
|
||||
"""Test that metadata is stored and retrieved correctly."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
metadata = {
|
||||
"text": "Steve Jobs",
|
||||
"label": "PERSON",
|
||||
"confidence": 0.95,
|
||||
"start": 0,
|
||||
"end": 10
|
||||
}
|
||||
|
||||
manager.track_entity(
|
||||
entity_id="person_1",
|
||||
source="biography.pdf",
|
||||
entity_type="person",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
# Retrieve and verify metadata
|
||||
lineage = manager.get_lineage("person_1")
|
||||
assert lineage is not None
|
||||
assert "metadata" in lineage
|
||||
stored_metadata = lineage["metadata"]
|
||||
assert stored_metadata["text"] == "Steve Jobs"
|
||||
assert stored_metadata["confidence"] == 0.95
|
||||
|
||||
def test_relationship_tracking(self):
|
||||
"""Test tracking relationships between entities."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track entities first
|
||||
manager.track_entity(
|
||||
entity_id="steve_jobs",
|
||||
source="doc.pdf",
|
||||
entity_type="person"
|
||||
)
|
||||
|
||||
manager.track_entity(
|
||||
entity_id="apple",
|
||||
source="doc.pdf",
|
||||
entity_type="organization"
|
||||
)
|
||||
|
||||
# Track relationship
|
||||
manager.track_relationship(
|
||||
relationship_id="rel_1",
|
||||
source="doc.pdf",
|
||||
subject="steve_jobs",
|
||||
predicate="founded",
|
||||
obj="apple"
|
||||
)
|
||||
|
||||
# Verify relationship tracked
|
||||
lineage = manager.get_lineage("rel_1")
|
||||
assert lineage is not None
|
||||
assert lineage["entity_id"] == "rel_1"
|
||||
|
||||
def test_batch_tracking_performance(self):
|
||||
"""Test batch tracking of multiple entities."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track 100 entities
|
||||
for i in range(100):
|
||||
manager.track_entity(
|
||||
entity_id=f"entity_{i}",
|
||||
source=f"document_{i % 10}.pdf",
|
||||
entity_type="test_entity",
|
||||
metadata={"index": i}
|
||||
)
|
||||
|
||||
# Verify all tracked
|
||||
for i in range(100):
|
||||
lineage = manager.get_lineage(f"entity_{i}")
|
||||
assert lineage is not None
|
||||
assert lineage["entity_id"] == f"entity_{i}"
|
||||
|
||||
def test_provenance_statistics(self):
|
||||
"""Test retrieving provenance statistics."""
|
||||
manager = ProvenanceManager()
|
||||
|
||||
# Track various items
|
||||
manager.track_entity("e1", "src1", entity_type="type1")
|
||||
manager.track_entity("e2", "src2", entity_type="type2")
|
||||
manager.track_relationship(
|
||||
relationship_id="r1",
|
||||
source="src1",
|
||||
metadata={"subject": "e1", "predicate": "relates", "object": "e2"}
|
||||
)
|
||||
|
||||
# Get statistics
|
||||
stats = manager.get_statistics()
|
||||
assert stats is not None
|
||||
assert stats["total_entries"] >= 3
|
||||
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
Test Provenance Storage Backends
|
||||
|
||||
Tests for InMemoryStorage and SQLiteStorage backends.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from semantica.provenance.schemas import ProvenanceEntry
|
||||
from semantica.provenance.storage import InMemoryStorage, SQLiteStorage
|
||||
|
||||
|
||||
class TestInMemoryStorage:
|
||||
"""Test InMemoryStorage backend."""
|
||||
|
||||
def test_store_and_retrieve(self):
|
||||
"""Test storing and retrieving entries."""
|
||||
storage = InMemoryStorage()
|
||||
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
|
||||
storage.store(entry)
|
||||
retrieved = storage.retrieve("entity_1")
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved.entity_id == "entity_1"
|
||||
|
||||
def test_retrieve_nonexistent(self):
|
||||
"""Test retrieving non-existent entry."""
|
||||
storage = InMemoryStorage()
|
||||
|
||||
retrieved = storage.retrieve("nonexistent")
|
||||
|
||||
assert retrieved is None
|
||||
|
||||
def test_retrieve_all(self):
|
||||
"""Test retrieving all entries."""
|
||||
storage = InMemoryStorage()
|
||||
|
||||
entry1 = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
entry2 = ProvenanceEntry(
|
||||
entity_id="entity_2",
|
||||
entity_type="chunk",
|
||||
activity_id="chunking"
|
||||
)
|
||||
|
||||
storage.store(entry1)
|
||||
storage.store(entry2)
|
||||
|
||||
all_entries = storage.retrieve_all()
|
||||
|
||||
assert len(all_entries) == 2
|
||||
|
||||
def test_retrieve_by_type(self):
|
||||
"""Test retrieving entries by type."""
|
||||
storage = InMemoryStorage()
|
||||
|
||||
entry1 = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
entry2 = ProvenanceEntry(
|
||||
entity_id="chunk_1",
|
||||
entity_type="chunk",
|
||||
activity_id="chunking"
|
||||
)
|
||||
|
||||
storage.store(entry1)
|
||||
storage.store(entry2)
|
||||
|
||||
entities = storage.retrieve_all(entity_type="entity")
|
||||
|
||||
assert len(entities) == 1
|
||||
assert entities[0].entity_type == "entity"
|
||||
|
||||
def test_trace_lineage(self):
|
||||
"""Test tracing lineage."""
|
||||
storage = InMemoryStorage()
|
||||
|
||||
# Create parent-child chain
|
||||
entry1 = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
entry2 = ProvenanceEntry(
|
||||
entity_id="entity_2",
|
||||
entity_type="entity",
|
||||
activity_id="transformation",
|
||||
parent_entity_id="entity_1"
|
||||
)
|
||||
entry3 = ProvenanceEntry(
|
||||
entity_id="entity_3",
|
||||
entity_type="entity",
|
||||
activity_id="transformation",
|
||||
parent_entity_id="entity_2"
|
||||
)
|
||||
|
||||
storage.store(entry1)
|
||||
storage.store(entry2)
|
||||
storage.store(entry3)
|
||||
|
||||
lineage = storage.trace_lineage("entity_3")
|
||||
|
||||
assert len(lineage) == 3
|
||||
entity_ids = [e.entity_id for e in lineage]
|
||||
assert "entity_1" in entity_ids
|
||||
assert "entity_2" in entity_ids
|
||||
assert "entity_3" in entity_ids
|
||||
|
||||
def test_clear(self):
|
||||
"""Test clearing storage."""
|
||||
storage = InMemoryStorage()
|
||||
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
|
||||
storage.store(entry)
|
||||
count = storage.clear()
|
||||
|
||||
assert count == 1
|
||||
assert len(storage.retrieve_all()) == 0
|
||||
|
||||
|
||||
class TestSQLiteStorage:
|
||||
"""Test SQLiteStorage backend."""
|
||||
|
||||
def test_store_and_retrieve(self):
|
||||
"""Test storing and retrieving entries."""
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as tmp:
|
||||
db_path = tmp.name
|
||||
|
||||
try:
|
||||
storage = SQLiteStorage(db_path)
|
||||
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
|
||||
storage.store(entry)
|
||||
retrieved = storage.retrieve("entity_1")
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved.entity_id == "entity_1"
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_persistence(self):
|
||||
"""Test data persistence across connections."""
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as tmp:
|
||||
db_path = tmp.name
|
||||
|
||||
try:
|
||||
# Store entry
|
||||
storage1 = SQLiteStorage(db_path)
|
||||
entry = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
storage1.store(entry)
|
||||
|
||||
# Retrieve with new connection
|
||||
storage2 = SQLiteStorage(db_path)
|
||||
retrieved = storage2.retrieve("entity_1")
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved.entity_id == "entity_1"
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_trace_lineage(self):
|
||||
"""Test tracing lineage in SQLite."""
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as tmp:
|
||||
db_path = tmp.name
|
||||
|
||||
try:
|
||||
storage = SQLiteStorage(db_path)
|
||||
|
||||
# Create parent-child chain
|
||||
entry1 = ProvenanceEntry(
|
||||
entity_id="entity_1",
|
||||
entity_type="entity",
|
||||
activity_id="extraction"
|
||||
)
|
||||
entry2 = ProvenanceEntry(
|
||||
entity_id="entity_2",
|
||||
entity_type="entity",
|
||||
activity_id="transformation",
|
||||
parent_entity_id="entity_1"
|
||||
)
|
||||
|
||||
storage.store(entry1)
|
||||
storage.store(entry2)
|
||||
|
||||
lineage = storage.trace_lineage("entity_2")
|
||||
|
||||
assert len(lineage) == 2
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
@@ -120,6 +120,184 @@ class TestProviderLimits:
|
||||
assert gen_config["top_k"] == 10
|
||||
assert gen_config["candidate_count"] == 2
|
||||
|
||||
class TestTemperatureParameter:
|
||||
"""Test that temperature=None omits the parameter from API calls."""
|
||||
|
||||
def test_openai_temperature_none_omitted(self):
|
||||
"""Verify temperature is NOT in kwargs when None."""
|
||||
from semantica.semantic_extract.providers import OpenAIProvider
|
||||
|
||||
with patch.object(OpenAIProvider, '_init_client', return_value=None):
|
||||
provider = OpenAIProvider(api_key="fake")
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices[0].message.content = "result"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt") # No temperature
|
||||
|
||||
call_kwargs = mock_client.chat.completions.create.call_args[1]
|
||||
assert "temperature" not in call_kwargs
|
||||
|
||||
def test_openai_temperature_explicit_included(self):
|
||||
"""Verify temperature IS in kwargs when explicitly set."""
|
||||
from semantica.semantic_extract.providers import OpenAIProvider
|
||||
|
||||
with patch.object(OpenAIProvider, '_init_client', return_value=None):
|
||||
provider = OpenAIProvider(api_key="fake")
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices[0].message.content = "result"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt", temperature=0.5)
|
||||
|
||||
call_kwargs = mock_client.chat.completions.create.call_args[1]
|
||||
assert call_kwargs["temperature"] == 0.5
|
||||
|
||||
def test_groq_temperature_none_omitted(self):
|
||||
"""Verify temperature is NOT in kwargs when None for Groq."""
|
||||
from semantica.semantic_extract.providers import GroqProvider
|
||||
|
||||
with patch.object(GroqProvider, '_init_client', return_value=None):
|
||||
provider = GroqProvider(api_key="fake")
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices[0].message.content = "result"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt") # No temperature
|
||||
|
||||
call_kwargs = mock_client.chat.completions.create.call_args[1]
|
||||
assert "temperature" not in call_kwargs
|
||||
|
||||
def test_groq_temperature_explicit_included(self):
|
||||
"""Verify temperature IS in kwargs when explicitly set for Groq."""
|
||||
from semantica.semantic_extract.providers import GroqProvider
|
||||
|
||||
with patch.object(GroqProvider, '_init_client', return_value=None):
|
||||
provider = GroqProvider(api_key="fake")
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices[0].message.content = "result"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt", temperature=0.7)
|
||||
|
||||
call_kwargs = mock_client.chat.completions.create.call_args[1]
|
||||
assert call_kwargs["temperature"] == 0.7
|
||||
|
||||
def test_gemini_temperature_none_omitted(self):
|
||||
"""Verify generation_config is None or empty when temperature not set for Gemini."""
|
||||
from semantica.semantic_extract.providers import GeminiProvider
|
||||
|
||||
with patch.object(GeminiProvider, '_init_client', return_value=None):
|
||||
provider = GeminiProvider(api_key="fake")
|
||||
mock_model = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = "result"
|
||||
mock_model.generate_content.return_value = mock_response
|
||||
provider.client = mock_model
|
||||
|
||||
provider.generate("prompt") # No temperature
|
||||
|
||||
call_kwargs = mock_model.generate_content.call_args[1]
|
||||
gen_config = call_kwargs.get("generation_config")
|
||||
# When no config params set, config is None or empty dict
|
||||
assert gen_config is None or "temperature" not in gen_config
|
||||
|
||||
def test_gemini_temperature_explicit_included(self):
|
||||
"""Verify temperature IS in generation_config when explicitly set for Gemini."""
|
||||
from semantica.semantic_extract.providers import GeminiProvider
|
||||
|
||||
with patch.object(GeminiProvider, '_init_client', return_value=None):
|
||||
provider = GeminiProvider(api_key="fake")
|
||||
mock_model = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = "result"
|
||||
mock_model.generate_content.return_value = mock_response
|
||||
provider.client = mock_model
|
||||
|
||||
provider.generate("prompt", temperature=0.8)
|
||||
|
||||
call_kwargs = mock_model.generate_content.call_args[1]
|
||||
gen_config = call_kwargs["generation_config"]
|
||||
assert gen_config["temperature"] == 0.8
|
||||
|
||||
def test_ollama_temperature_none_omitted(self):
|
||||
"""Verify options is None or has no temperature when None for Ollama."""
|
||||
from semantica.semantic_extract.providers import OllamaProvider
|
||||
|
||||
with patch.object(OllamaProvider, '_init_client', return_value=None):
|
||||
provider = OllamaProvider()
|
||||
mock_client = MagicMock()
|
||||
mock_response = {"response": "result"}
|
||||
mock_client.generate.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt") # No temperature
|
||||
|
||||
call_kwargs = mock_client.generate.call_args[1]
|
||||
options = call_kwargs.get("options")
|
||||
# When no options params set, options is None or empty dict
|
||||
assert options is None or "temperature" not in options
|
||||
|
||||
def test_ollama_temperature_explicit_included(self):
|
||||
"""Verify temperature IS in options when explicitly set for Ollama."""
|
||||
from semantica.semantic_extract.providers import OllamaProvider
|
||||
|
||||
with patch.object(OllamaProvider, '_init_client', return_value=None):
|
||||
provider = OllamaProvider()
|
||||
mock_client = MagicMock()
|
||||
mock_response = {"response": "result"}
|
||||
mock_client.generate.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt", temperature=0.3)
|
||||
|
||||
call_kwargs = mock_client.generate.call_args[1]
|
||||
options = call_kwargs.get("options", {})
|
||||
assert options["temperature"] == 0.3
|
||||
|
||||
def test_deepseek_temperature_none_omitted(self):
|
||||
"""Verify temperature is NOT in kwargs when None for DeepSeek."""
|
||||
from semantica.semantic_extract.providers import DeepSeekProvider
|
||||
|
||||
with patch.object(DeepSeekProvider, '_init_client', return_value=None):
|
||||
provider = DeepSeekProvider(api_key="fake")
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices[0].message.content = "result"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt") # No temperature
|
||||
|
||||
call_kwargs = mock_client.chat.completions.create.call_args[1]
|
||||
assert "temperature" not in call_kwargs
|
||||
|
||||
def test_deepseek_temperature_explicit_included(self):
|
||||
"""Verify temperature IS in kwargs when explicitly set for DeepSeek."""
|
||||
from semantica.semantic_extract.providers import DeepSeekProvider
|
||||
|
||||
with patch.object(DeepSeekProvider, '_init_client', return_value=None):
|
||||
provider = DeepSeekProvider(api_key="fake")
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices[0].message.content = "result"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
provider.client = mock_client
|
||||
|
||||
provider.generate("prompt", temperature=0.9)
|
||||
|
||||
call_kwargs = mock_client.chat.completions.create.call_args[1]
|
||||
assert call_kwargs["temperature"] == 0.9
|
||||
|
||||
|
||||
class TestChunkingDefaults:
|
||||
"""Test that chunking defaults have been increased."""
|
||||
|
||||
|
||||
@@ -9,13 +9,10 @@ import importlib
|
||||
PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, PARENT_DIR)
|
||||
|
||||
# Force reload modules
|
||||
# Import modules
|
||||
import semantica.utils.exceptions
|
||||
importlib.reload(semantica.utils.exceptions)
|
||||
import semantica.semantic_extract.methods
|
||||
importlib.reload(semantica.semantic_extract.methods)
|
||||
import semantica.semantic_extract.triplet_extractor
|
||||
importlib.reload(semantica.semantic_extract.triplet_extractor)
|
||||
|
||||
from semantica.semantic_extract.methods import extract_entities_llm, extract_relations_llm, extract_triplets_llm
|
||||
from semantica.semantic_extract.triplet_extractor import TripletExtractor, Triplet
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
"""
|
||||
Test suite for JenaStore empty graph bug fix (#257, #258).
|
||||
|
||||
This test suite validates the fix for the bug where JenaStore raised
|
||||
'ProcessingError: Graph not initialized' when operating on empty (but initialized) graphs.
|
||||
|
||||
The fix replaced implicit `if not self.graph:` checks with explicit `if self.graph is None:`
|
||||
checks to properly distinguish between:
|
||||
- None (uninitialized graph)
|
||||
- Empty graph (initialized with 0 triplets)
|
||||
|
||||
Contributor: @ZohaibHassan16
|
||||
Issue: #257, #258
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from semantica.triplet_store import JenaStore
|
||||
from semantica.semantic_extract.triplet_extractor import Triplet
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
class TestJenaStoreEmptyGraph:
|
||||
"""Test JenaStore operations on empty graphs."""
|
||||
|
||||
def test_empty_graph_initialization(self):
|
||||
"""Test that empty graph initializes correctly."""
|
||||
store = JenaStore()
|
||||
|
||||
# Graph should be initialized (not None)
|
||||
assert store.graph is not None
|
||||
|
||||
# Graph should be empty (0 triplets)
|
||||
assert len(store.graph) == 0
|
||||
|
||||
def test_add_triplets_to_empty_graph(self):
|
||||
"""Test adding triplets to empty initialized graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Verify graph is empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Add triplets to empty graph (should not raise error)
|
||||
triplets = [
|
||||
Triplet(subject="http://example.org/Alice",
|
||||
predicate="http://example.org/knows",
|
||||
object="http://example.org/Bob"),
|
||||
Triplet(subject="http://example.org/Bob",
|
||||
predicate="http://example.org/age",
|
||||
object="30")
|
||||
]
|
||||
|
||||
result = store.add_triplets(triplets)
|
||||
|
||||
# Verify success
|
||||
assert result["success"] is True
|
||||
assert result["added"] == 2
|
||||
assert len(store.graph) == 2
|
||||
|
||||
def test_get_triplets_from_empty_graph(self):
|
||||
"""Test getting triplets from empty initialized graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Verify graph is empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Get triplets from empty graph (should return empty list, not error)
|
||||
triplets = store.get_triplets()
|
||||
|
||||
# Verify returns empty list
|
||||
assert triplets == []
|
||||
assert isinstance(triplets, list)
|
||||
|
||||
def test_get_triplets_with_filters_on_empty_graph(self):
|
||||
"""Test filtered queries on empty graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Query with subject filter
|
||||
triplets = store.get_triplets(subject="http://example.org/Alice")
|
||||
assert triplets == []
|
||||
|
||||
# Query with predicate filter
|
||||
triplets = store.get_triplets(predicate="http://example.org/knows")
|
||||
assert triplets == []
|
||||
|
||||
# Query with object filter
|
||||
triplets = store.get_triplets(object="http://example.org/Bob")
|
||||
assert triplets == []
|
||||
|
||||
# Query with multiple filters
|
||||
triplets = store.get_triplets(
|
||||
subject="http://example.org/Alice",
|
||||
predicate="http://example.org/knows"
|
||||
)
|
||||
assert triplets == []
|
||||
|
||||
def test_delete_triplet_from_empty_graph(self):
|
||||
"""Test deleting triplet from empty graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Verify graph is empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Attempt to delete from empty graph (should not raise error)
|
||||
triplet = Triplet(
|
||||
subject="http://example.org/Alice",
|
||||
predicate="http://example.org/knows",
|
||||
object="http://example.org/Bob"
|
||||
)
|
||||
|
||||
# Should succeed gracefully (nothing to delete)
|
||||
result = store.delete_triplet(triplet)
|
||||
assert result["success"] is True
|
||||
|
||||
# Graph should still be empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
def test_execute_sparql_on_empty_graph(self):
|
||||
"""Test SPARQL execution on empty graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Verify graph is empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Execute SELECT query on empty graph
|
||||
query = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"
|
||||
result = store.execute_sparql(query)
|
||||
|
||||
# Should return empty results, not error
|
||||
assert result["success"] is True
|
||||
assert result["bindings"] == []
|
||||
assert result["variables"] == ["s", "p", "o"]
|
||||
|
||||
def test_execute_sparql_ask_on_empty_graph(self):
|
||||
"""Test SPARQL ASK query on empty graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# ASK query on empty graph
|
||||
query = "ASK WHERE { ?s ?p ?o }"
|
||||
result = store.execute_sparql(query)
|
||||
|
||||
# Should execute without error
|
||||
assert result["success"] is True
|
||||
|
||||
def test_execute_sparql_construct_on_empty_graph(self):
|
||||
"""Test SPARQL CONSTRUCT query on empty graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# CONSTRUCT query on empty graph
|
||||
query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"
|
||||
result = store.execute_sparql(query)
|
||||
|
||||
# Should execute without error
|
||||
assert result["success"] is True
|
||||
|
||||
def test_serialize_empty_graph(self):
|
||||
"""Test serialization of empty graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Verify graph is empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Serialize empty graph in Turtle format
|
||||
turtle = store.serialize(format="turtle")
|
||||
|
||||
# Should return valid (empty) RDF, not error
|
||||
assert isinstance(turtle, str)
|
||||
# Empty graph serialization should be minimal
|
||||
assert len(turtle) < 100 # Just namespace declarations
|
||||
|
||||
def test_serialize_empty_graph_multiple_formats(self):
|
||||
"""Test serialization of empty graph in multiple formats."""
|
||||
store = JenaStore()
|
||||
|
||||
# Turtle
|
||||
turtle = store.serialize(format="turtle")
|
||||
assert isinstance(turtle, str)
|
||||
|
||||
# RDF/XML
|
||||
rdfxml = store.serialize(format="xml")
|
||||
assert isinstance(rdfxml, str)
|
||||
|
||||
# N-Triples
|
||||
ntriples = store.serialize(format="nt")
|
||||
assert isinstance(ntriples, str)
|
||||
|
||||
def test_graph_becomes_empty_after_deletion(self):
|
||||
"""Test graph that becomes empty after deleting all triplets."""
|
||||
store = JenaStore()
|
||||
|
||||
# Add a triplet
|
||||
triplet = Triplet(
|
||||
subject="http://example.org/Alice",
|
||||
predicate="http://example.org/knows",
|
||||
object="http://example.org/Bob"
|
||||
)
|
||||
store.add_triplets([triplet])
|
||||
assert len(store.graph) == 1
|
||||
|
||||
# Delete the triplet (graph becomes empty)
|
||||
store.delete_triplet(triplet)
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Verify operations still work on now-empty graph
|
||||
triplets = store.get_triplets()
|
||||
assert triplets == []
|
||||
|
||||
# Add another triplet (should work)
|
||||
new_triplet = Triplet(
|
||||
subject="http://example.org/Charlie",
|
||||
predicate="http://example.org/age",
|
||||
object="25"
|
||||
)
|
||||
result = store.add_triplets([new_triplet])
|
||||
assert result["success"] is True
|
||||
assert len(store.graph) == 1
|
||||
|
||||
def test_rapid_add_delete_operations(self):
|
||||
"""Test rapid add/delete operations that oscillate between empty/non-empty."""
|
||||
store = JenaStore()
|
||||
|
||||
triplet = Triplet(
|
||||
subject="http://example.org/Test",
|
||||
predicate="http://example.org/value",
|
||||
object="123"
|
||||
)
|
||||
|
||||
# Oscillate 10 times
|
||||
for i in range(10):
|
||||
# Add (graph becomes non-empty)
|
||||
store.add_triplets([triplet])
|
||||
assert len(store.graph) == 1
|
||||
|
||||
# Delete (graph becomes empty)
|
||||
store.delete_triplet(triplet)
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Verify operations work on empty graph
|
||||
result = store.get_triplets()
|
||||
assert result == []
|
||||
|
||||
def test_uninitialized_graph_raises_error(self):
|
||||
"""Test that None (uninitialized) graph raises appropriate errors."""
|
||||
store = JenaStore()
|
||||
|
||||
# Manually set graph to None (simulating uninitialized state)
|
||||
store.graph = None
|
||||
|
||||
# add_triplets should raise ProcessingError
|
||||
with pytest.raises(ProcessingError, match="Graph not initialized"):
|
||||
store.add_triplets([
|
||||
Triplet(subject="s", predicate="p", object="o")
|
||||
])
|
||||
|
||||
# delete_triplet should raise ProcessingError
|
||||
with pytest.raises(ProcessingError, match="Graph not initialized"):
|
||||
store.delete_triplet(
|
||||
Triplet(subject="s", predicate="p", object="o")
|
||||
)
|
||||
|
||||
# execute_sparql should raise ProcessingError
|
||||
with pytest.raises(ProcessingError, match="Graph not initialized"):
|
||||
store.execute_sparql("SELECT * WHERE { ?s ?p ?o }")
|
||||
|
||||
def test_get_triplets_returns_empty_list_for_none_graph(self):
|
||||
"""Test that get_triplets returns [] for None graph (not error)."""
|
||||
store = JenaStore()
|
||||
|
||||
# Manually set graph to None
|
||||
store.graph = None
|
||||
|
||||
# get_triplets should return empty list (graceful handling)
|
||||
triplets = store.get_triplets()
|
||||
assert triplets == []
|
||||
|
||||
def test_serialize_returns_empty_string_for_none_graph(self):
|
||||
"""Test that serialize returns empty string for None graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Manually set graph to None
|
||||
store.graph = None
|
||||
|
||||
# serialize should return empty string (graceful handling)
|
||||
result = store.serialize()
|
||||
assert result == ""
|
||||
|
||||
def test_create_model_with_empty_graph(self):
|
||||
"""Test create_model with empty graph."""
|
||||
store = JenaStore()
|
||||
|
||||
# Verify graph is empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Create model should work
|
||||
model_info = store.create_model()
|
||||
|
||||
assert model_info["triplet_count"] == 0
|
||||
assert "model_id" in model_info
|
||||
|
||||
def test_empty_graph_with_concurrent_operations(self):
|
||||
"""Test concurrent operations on empty graph."""
|
||||
import threading
|
||||
|
||||
store = JenaStore()
|
||||
errors = []
|
||||
|
||||
def add_triplet():
|
||||
try:
|
||||
# Add triplet (may see other threads' triplets due to race condition)
|
||||
store.add_triplets([
|
||||
Triplet(
|
||||
subject=f"http://example.org/Entity{threading.current_thread().ident}",
|
||||
predicate="http://example.org/type",
|
||||
object="Test"
|
||||
)
|
||||
])
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
# Run 5 concurrent threads
|
||||
threads = [threading.Thread(target=add_triplet) for _ in range(5)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# No errors should occur
|
||||
assert len(errors) == 0
|
||||
|
||||
# Graph should have 5 triplets
|
||||
assert len(store.graph) == 5
|
||||
|
||||
def test_benchmarking_scenario_empty_graph(self):
|
||||
"""Test benchmarking scenario with fresh empty graph."""
|
||||
# This simulates the benchmarking suite use case
|
||||
store = JenaStore()
|
||||
|
||||
# Benchmarking starts with empty graph
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Benchmark: Add 100 triplets
|
||||
triplets = [
|
||||
Triplet(
|
||||
subject=f"http://example.org/Entity{i}",
|
||||
predicate="http://example.org/type",
|
||||
object="BenchmarkEntity"
|
||||
)
|
||||
for i in range(100)
|
||||
]
|
||||
|
||||
result = store.add_triplets(triplets)
|
||||
assert result["success"] is True
|
||||
assert result["added"] == 100
|
||||
|
||||
# Benchmark: Query all
|
||||
all_triplets = store.get_triplets()
|
||||
assert len(all_triplets) == 100
|
||||
|
||||
# Benchmark: SPARQL query
|
||||
sparql_result = store.execute_sparql("SELECT ?s WHERE { ?s ?p ?o }")
|
||||
assert sparql_result["success"] is True
|
||||
assert len(sparql_result["bindings"]) == 100
|
||||
|
||||
|
||||
class TestJenaStoreEdgeCases:
|
||||
"""Additional edge case tests for JenaStore."""
|
||||
|
||||
def test_empty_graph_with_blank_nodes(self):
|
||||
"""Test empty graph operations with blank nodes."""
|
||||
store = JenaStore()
|
||||
|
||||
# Add triplet with blank node
|
||||
triplet = Triplet(
|
||||
subject="_:blank1",
|
||||
predicate="http://example.org/type",
|
||||
object="BlankNode"
|
||||
)
|
||||
|
||||
result = store.add_triplets([triplet])
|
||||
assert result["success"] is True
|
||||
|
||||
def test_empty_graph_with_very_long_uris(self):
|
||||
"""Test empty graph with very long URIs (>1000 chars)."""
|
||||
store = JenaStore()
|
||||
|
||||
long_uri = "http://example.org/" + "a" * 1000
|
||||
triplet = Triplet(
|
||||
subject=long_uri,
|
||||
predicate="http://example.org/type",
|
||||
object="LongURI"
|
||||
)
|
||||
|
||||
result = store.add_triplets([triplet])
|
||||
assert result["success"] is True
|
||||
assert len(store.graph) == 1
|
||||
|
||||
def test_empty_graph_with_unicode_literals(self):
|
||||
"""Test empty graph with Unicode literals."""
|
||||
store = JenaStore()
|
||||
|
||||
triplet = Triplet(
|
||||
subject="http://example.org/Entity",
|
||||
predicate="http://example.org/name",
|
||||
object="こんにちは世界" # Japanese: Hello World
|
||||
)
|
||||
|
||||
result = store.add_triplets([triplet])
|
||||
assert result["success"] is True
|
||||
assert len(store.graph) == 1
|
||||
|
||||
def test_empty_graph_cleared_and_reused(self):
|
||||
"""Test graph that is cleared and reused multiple times."""
|
||||
store = JenaStore()
|
||||
|
||||
for iteration in range(3):
|
||||
# Add triplets
|
||||
triplets = [
|
||||
Triplet(
|
||||
subject=f"http://example.org/Entity{i}",
|
||||
predicate="http://example.org/iteration",
|
||||
object=str(iteration)
|
||||
)
|
||||
for i in range(10)
|
||||
]
|
||||
store.add_triplets(triplets)
|
||||
assert len(store.graph) == 10
|
||||
|
||||
# Clear graph by deleting all
|
||||
for triplet in triplets:
|
||||
store.delete_triplet(triplet)
|
||||
|
||||
# Verify empty
|
||||
assert len(store.graph) == 0
|
||||
|
||||
# Verify operations work
|
||||
result = store.get_triplets()
|
||||
assert result == []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user