Compare commits

..
Author SHA1 Message Date
KaifAhmad1 0aaca1bb7d Fix(provenance): Resolve metadata crash and broken lineage chains
- Fix: Handle stringified JSON in get_lineage metadata aggregation to prevent ValueError.
- Fix: Auto-detect and link source as parent_entity_id in 	rack_entity to ensure cross-module lineage continuity.
- Verified: 	est_cross_module_lineage passed.
2026-02-02 21:52:33 +05:30
KaifAhmad1 8faeb606d7 Fix(provenance): Resolve backward compatibility and metadata issues
- Fix: Provide versioned source history in ProvenanceManager.track_entity to support correct get_all_sources behavior.
- Fix: Ensure get_lineage aggregates and returns metadata fields correctly.
- Fix: Update 	est_real_module_integration.py and 	est_semantic_extract_provenance.py to match correct 	rack_relationship API signature.
- Verified: All provenance tests passed (237/237).
2026-02-02 21:36:29 +05:30
Mohd Kaif be6b8afedc Delete examples directory 2026-02-02 18:34:55 +05:30
Mohd Kaif 4baa026a3e Update README.md 2026-02-02 17:40:16 +05:30
Mohd Kaif 515c4ee205 Merge pull request #269 from Hawksight-AI/integrations
feat: Add integrations folder for framework integrations
2026-02-02 17:34:07 +05:30
KaifAhmad1 d884b42472 feat: Add integrations folder for framework integrations
- Created integrations/ folder at repository root for optional framework integrations
- Moved integrations folder from semantica/integrations/ to root-level integrations/
- Added __init__.py with documentation for future integrations (Google ADK, Claude Agent SDK, Agno)
- Keeps core semantica package lean while enabling ecosystem integrations
- Each integration will be self-contained and installable via extras_require
2026-02-02 17:31:45 +05:30
Mohd Kaif f3abeb528b Merge pull request #268 from Hawksight-AI/docs
[DOCS] Replace Semantica Logo with New Clean Design
2026-02-02 15:20:03 +05:30
KaifAhmad1 78e552853d [DOCS] Replace Semantica Logo with New Clean Design - Fixes #266
- Updated README.md with new logo reference
- Updated docs/index.md with new logo reference
- Updated docs/DOCS_README.md documentation
- Added new clean, professional logo (Semantica Updated Logo.png)
- Removed old illustrated logo (semantica_logo.png)

The new logo is minimal, scales well, and better represents Semantica as an enterprise-grade semantic layer.
2026-02-02 15:15:55 +05:30
Mohd Kaif 8dc1a664f1 Add files via upload
Adds the updated Semantica logo and updates references in the README and documentation.
This improves visual consistency across project assets.
2026-02-02 14:39:32 +05:30
Mohd Kaif 797cb61a3f Merge pull request #267 from ItzCobaltboy/readme-typo-fix
docs: Fix typo in README (choas -> chaos)
2026-02-02 13:53:38 +05:30
Cobaltboy d223a8ce23 Fix typo in README (choas -> chaos) 2026-02-02 13:39:53 +05:30
Mohd Kaif 3da10149ee Merge pull request #263 from Hawksight-AI/integrations
feat: Add integrations module placeholder for future framework integr…
2026-02-01 22:36:57 +05:30
KaifAhmad1 c8e9e576fc feat: Add integrations module placeholder for future framework integrations 2026-02-01 22:34:41 +05:30
KaifAhmad1 f5ba8312a7 docs(changelog): clarify compliance infrastructure instead of support 2026-02-01 16:43:39 +05:30
KaifAhmad1 f95a1ccfd1 Merge branch 'main' of https://github.com/Hawksight-AI/semantica 2026-02-01 16:41:45 +05:30
KaifAhmad1 af52a48289 docs(changelog): update with PRs #254, #248, #252, #258, #239 and contributor credits 2026-02-01 16:41:30 +05:30
Mohd Kaif bce53a9fe3 Merge pull request #252 from F0rt1s/fix/temperature-compatibility
fix: allow temperature=None to use model defaults
2026-02-01 15:40:05 +05:30
Mohd Kaif 937d5f3f1c Merge pull request #258 from ZohaibHassan16/fix/jena-empty-graph-bug
Fix: JenaStore crash on empty graph operations (#257)
2026-02-01 15:07:38 +05:30
ZohaibHassan16 31c90b0d19 Fix: JenaStore empty graph issue (Issue #257) 2026-02-01 14:19:21 +05:00
Steffen John 78664ec5f6 test: add tests for temperature=None behavior
Verify that temperature parameter is omitted from API calls when None,
allowing models to use their defaults. Tests cover OpenAI, Groq, Gemini,
Ollama, and DeepSeek providers.
2026-01-31 22:16:09 +01:00
Mohd Kaif d2d229125b Delete PROVENANCE_PR.md 2026-01-31 20:32:57 +05:30
Mohd Kaif 7de518432b Merge pull request #255 from Hawksight-AI/provenance
Fix MkDocs CI: Add provenance to nav, update CHANGELOG, add PR descri…
2026-01-31 20:32:16 +05:30
KaifAhmad1 079ae5cd10 Fix MkDocs CI: Add provenance to nav, update CHANGELOG, add PR description 2026-01-31 20:29:03 +05:30
Mohd Kaif 060780eb7e Merge pull request #254 from Hawksight-AI/provenance
Add W3C PROV-O Compliant Provenance Tracking
2026-01-31 20:23:38 +05:30
KaifAhmad1 6391dcdf72 Add comprehensive W3C PROV-O compliant provenance tracking module
- Implemented provenance tracking across all 17 Semantica modules
- Added W3C PROV-O compliant schemas (prov:Entity, prov:Activity, prov:Agent, prov:wasDerivedFrom)
- Created ProvenanceManager with InMemory and SQLite storage backends
- Implemented SHA-256 integrity verification for tamper detection
- Added bridge axiom support for domain transformations (L1→L2→L3)
- Created provenance-enabled versions of all modules (opt-in with provenance=True)
- Added comprehensive test suite (237 tests covering edge cases and real scenarios)
- Updated README with accurate claims and compliance disclaimers
- Added complete documentation (usage guide and API reference)
- Zero breaking changes - fully backward compatible
2026-01-31 20:11:38 +05:30
Steffen John d172d7da62 fix: allow temperature=None to use model defaults
Models like gpt-5-mini only support specific temperature values.
This change allows temperature=None to mean "use model's default"
by omitting the parameter from API calls entirely.

Changes:
- Add _add_if_set helper to BaseProvider for cleaner param handling
- Update all providers to conditionally include temperature
- Remove hardcoded temperature defaults from entry points
- Keep 0.7 default for HuggingFace (local models)
- Keep 0.1 fallback for generate_typed (structured output)
2026-01-30 19:44:49 +01:00
Mohd Kaif d7575f30c3 Merge pull request #248 from Hawksight-AI/change-management
Add Enhanced Change Management Module with comprehensive testing and …
2026-01-30 16:05:49 +05:30
58 changed files with 10110 additions and 362 deletions
+40 -5
View File
@@ -9,15 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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 +42,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
+100 -37
View File
@@ -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
View File
@@ -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

+1 -1
View File
@@ -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>
+667
View File
@@ -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).
-147
View File
@@ -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}")
+7
View File
@@ -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.
"""
+2
View File
@@ -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
@@ -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']
+54
View File
@@ -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']
+1 -1
View File
@@ -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']
+58
View File
@@ -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']
+67
View File
@@ -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']
+97 -11
View File
@@ -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,
+386
View File
@@ -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']
+1 -1
View File
@@ -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")
+53
View File
@@ -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']
+58
View File
@@ -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']
+67
View File
@@ -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']
+69
View File
@@ -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"
+435
View File
@@ -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
+178
View File
@@ -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
+574
View File
@@ -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
+277
View File
@@ -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)
+489
View File
@@ -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']
+2 -2
View File
@@ -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:
+53 -139
View File
@@ -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',
]
+132 -7
View File
@@ -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
+10 -10
View File
@@ -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

+17
View File
@@ -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
"""
+499
View File
@@ -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")
+252
View File
@@ -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
+491
View File
@@ -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
+280
View File
@@ -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
+177
View File
@@ -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")
+257
View File
@@ -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 lineage["source"] == source
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
+156
View File
@@ -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["first_seen"] < lineage_v2["first_seen"]
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])
+164
View File
@@ -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
+218
View File
@@ -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."""