mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-30 04:40:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84b90b45a2 | ||
|
|
d3366bbcf0 | ||
|
|
8b6e8608c3 | ||
|
|
315e2edb14 | ||
|
|
a93ed8f13a | ||
|
|
921bf18041 | ||
|
|
971b42631e | ||
|
|
3e4bc8521f | ||
|
|
521e2e27d8 | ||
|
|
c8f745cef0 | ||
|
|
c307011311 | ||
|
|
79ff296001 | ||
|
|
2a28e833b9 | ||
|
|
30cede84c7 | ||
|
|
9c8d0c032b | ||
|
|
e0e42dc539 | ||
|
|
f59fe1d689 | ||
|
|
1cfbf626d0 |
-237
@@ -1,237 +0,0 @@
|
|||||||
# Add Intelligence Cookbook Notebooks with MCP, Agents, and Orchestrator-Worker Pattern
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Add comprehensive intelligence-focused notebooks to `cookbook/use_cases/intelligence/` with complete end-to-end pipelines. The **Intelligence Analysis** notebook will use the **Orchestrator-Worker Pattern** with detailed graph analytics, hybrid RAG, and ontology building. Update documentation in `docs/cookbook.md` and `docs/use-cases.md`.
|
|
||||||
|
|
||||||
## New Notebooks to Create
|
|
||||||
|
|
||||||
### 1. Criminal Network Analysis (`Criminal_Network_Analysis.ipynb`)
|
|
||||||
Complete pipeline from data sources to GraphRAG with agent-based workflows:
|
|
||||||
- **Data Sources**: Ingest from police reports, court records, surveillance data, communication logs
|
|
||||||
- **MCP Integration**: Utilize MCP for accessing public records databases, court records APIs, and real-time data streams
|
|
||||||
- **Semantica Agents**:
|
|
||||||
- Data Gathering Agent (autonomous data collection with AgentMemory)
|
|
||||||
- Network Analysis Agent (graph analytics and community detection)
|
|
||||||
- Pattern Detection Agent (identifying suspicious patterns)
|
|
||||||
- Report Generation Agent (compiling intelligence reports)
|
|
||||||
- **Agent Coordination**: Use Pipeline module for parallel agent workflows
|
|
||||||
- **Agent Memory**: AgentMemory for persistent context across interactions
|
|
||||||
- **Complete Pipeline**: Data sources → MCP → Parsing → Extraction → KG → Graph Analytics → GraphRAG → Agent Analysis → Visualization → Reporting
|
|
||||||
|
|
||||||
### 2. Law Enforcement and Forensics (`Law_Enforcement_Forensics.ipynb`)
|
|
||||||
Complete forensic analysis pipeline with agent-based workflows:
|
|
||||||
- **Data Sources**: Case files, evidence logs, witness statements, forensic reports, crime scene data
|
|
||||||
- **Semantica Agents**:
|
|
||||||
- Evidence Collection Agent (autonomous evidence gathering)
|
|
||||||
- Timeline Analysis Agent (temporal case timelines)
|
|
||||||
- Cross-Case Correlation Agent (connections across cases)
|
|
||||||
- Forensic Report Agent (comprehensive report generation)
|
|
||||||
- **Agent Coordination**: Multi-agent pipeline for parallel evidence processing
|
|
||||||
- **Agent Memory**: Persistent memory for case context and evidence chains
|
|
||||||
- **Complete Pipeline**: Case files → Parsing → Evidence Extraction → Temporal KG → Graph Analytics → GraphRAG → Agent Analysis → Visualization → Reporting
|
|
||||||
|
|
||||||
### 3. Intelligence Analysis (`Intelligence_Analysis.ipynb`) - **ORCHESTRATOR-WORKER PATTERN**
|
|
||||||
Comprehensive intelligence analysis using **Orchestrator-Worker Pattern** with detailed implementation:
|
|
||||||
|
|
||||||
#### Orchestrator-Worker Architecture:
|
|
||||||
- **Orchestrator**: ExecutionEngine coordinates all workers using PipelineBuilder and ParallelismManager
|
|
||||||
- **Worker 1 - Data Ingestion Worker**: Handles multi-source data ingestion (FileIngestor, WebIngestor, StreamIngestor, FeedIngestor, DBIngestor)
|
|
||||||
- **Worker 2 - Ontology Building Worker**: Complete 6-stage ontology generation pipeline
|
|
||||||
- Stage 1: Semantic Network Parsing (extract domain concepts)
|
|
||||||
- Stage 2: YAML-to-Definition (transform concepts to class definitions)
|
|
||||||
- Stage 3: Definition-to-Types (map to OWL types)
|
|
||||||
- Stage 4: Hierarchy Generation (build taxonomic structures)
|
|
||||||
- Stage 5: TTL Generation (generate OWL/Turtle syntax)
|
|
||||||
- Stage 6: Symbolic Validation (HermiT/Pellet reasoning)
|
|
||||||
- **Worker 3 - Graph Construction Worker**: Builds knowledge graphs (GraphBuilder, TemporalGraphQuery)
|
|
||||||
- **Worker 4 - Graph Analytics Worker**: Comprehensive graph analytics including:
|
|
||||||
- Centrality Measures: PageRank, Betweenness, Closeness, Eigenvector
|
|
||||||
- Community Detection: Louvain algorithm
|
|
||||||
- Connectivity Analysis: Path finding, shortest paths, connectivity metrics
|
|
||||||
- Graph Metrics: Density, clustering coefficient, diameter, radius
|
|
||||||
- **Worker 5 - Hybrid RAG Worker**: Complete hybrid RAG implementation:
|
|
||||||
- Vector Store setup with embeddings
|
|
||||||
- Knowledge Graph queries
|
|
||||||
- Hybrid Search (combining vector similarity + graph traversal)
|
|
||||||
- Context Retrieval (ContextRetriever)
|
|
||||||
- Query Orchestration across KG and vector store
|
|
||||||
- **Worker 6 - Intelligence Analysis Worker**: Threat assessment, geospatial analysis, pattern detection
|
|
||||||
- **Worker 7 - Report Generation Worker**: Compiles comprehensive intelligence reports
|
|
||||||
|
|
||||||
#### Complete Features:
|
|
||||||
- **Data Sources**: OSINT feeds, threat intelligence, social media, news, public records, geospatial data
|
|
||||||
- **MCP Integration**: Real-time data fetching, web scraping, API integration, browser automation for OSINT
|
|
||||||
- **Agent Memory**: Persistent memory for threat context and intelligence history
|
|
||||||
- **Complete Pipeline**: OSINT sources → MCP → Orchestrator → Parallel Workers → Ontology → KG → Graph Analytics → Hybrid RAG → Intelligence Analysis → Visualization → Reporting
|
|
||||||
|
|
||||||
## Files to Create/Modify
|
|
||||||
|
|
||||||
### New Notebooks (in `cookbook/use_cases/intelligence/`)
|
|
||||||
- `Criminal_Network_Analysis.ipynb`
|
|
||||||
- `Law_Enforcement_Forensics.ipynb`
|
|
||||||
- `Intelligence_Analysis.ipynb` (with Orchestrator-Worker Pattern)
|
|
||||||
|
|
||||||
### Documentation Updates
|
|
||||||
- `docs/cookbook.md` - Add new notebooks to Intelligence section
|
|
||||||
- `docs/use-cases.md` - Add use case cards for Criminal Network Analysis and Law Enforcement & Forensics
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Intelligence Analysis - Orchestrator-Worker Pipeline Structure:
|
|
||||||
|
|
||||||
1. **Orchestrator Setup** - Initialize ExecutionEngine, PipelineBuilder, ParallelismManager
|
|
||||||
2. **Data Sources** - Multiple ingestion (FileIngestor, DBIngestor, WebIngestor, StreamIngestor, FeedIngestor)
|
|
||||||
3. **MCP Integration** - External data access, web scraping, browser automation
|
|
||||||
4. **Worker 1 - Data Ingestion Worker** - Parallel data gathering from multiple sources
|
|
||||||
5. **Data Parsing** - Parse structured/unstructured data (JSONParser, XMLParser, CSVParser, DocumentParser, StructuredDataParser)
|
|
||||||
6. **Data Normalization** - Clean and standardize (TextNormalizer, DataNormalizer)
|
|
||||||
7. **Entity & Relation Extraction** - Extract entities, relationships, events (NERExtractor, RelationExtractor, TripleExtractor, EventDetector)
|
|
||||||
8. **Worker 2 - Ontology Building Worker** - Complete 6-stage ontology generation:
|
|
||||||
- Use OntologyGenerator, ClassInferrer, PropertyGenerator
|
|
||||||
- Generate OWL/Turtle with OWLGenerator
|
|
||||||
- Validate with OntologyValidator (HermiT/Pellet)
|
|
||||||
9. **Worker 3 - Graph Construction Worker** - Build knowledge graphs:
|
|
||||||
- GraphBuilder for entity/relationship graphs
|
|
||||||
- TemporalGraphQuery for time-aware graphs
|
|
||||||
10. **Worker 4 - Graph Analytics Worker** - All graph analytics:
|
|
||||||
- GraphAnalyzer: PageRank, Betweenness, Closeness, Eigenvector centrality
|
|
||||||
- CommunityDetector: Louvain community detection
|
|
||||||
- ConnectivityAnalyzer: Path finding, shortest paths, connectivity
|
|
||||||
- CentralityCalculator: All centrality measures
|
|
||||||
- Graph metrics: density, clustering, diameter, radius
|
|
||||||
11. **Worker 5 - Hybrid RAG Worker** - Complete hybrid RAG:
|
|
||||||
- EmbeddingGenerator: Generate embeddings for entities and text
|
|
||||||
- VectorStore: Store and index embeddings
|
|
||||||
- HybridSearch: Combine vector similarity + graph queries
|
|
||||||
- ContextRetriever: Retrieve relevant context from KG and vectors
|
|
||||||
- Query orchestration: Coordinate queries across KG and vector store
|
|
||||||
12. **Worker 6 - Intelligence Analysis Worker** - Threat assessment, geospatial analysis, pattern detection
|
|
||||||
13. **Agent Memory Integration** - Store and retrieve agent context using AgentMemory
|
|
||||||
14. **Orchestrator Coordination** - Coordinate all workers with parallel execution
|
|
||||||
15. **Visualization** - Network graphs, analytics dashboards, maps (KGVisualizer, AnalyticsVisualizer, TemporalVisualizer)
|
|
||||||
16. **Worker 7 - Report Generation Worker** - Compile comprehensive intelligence reports
|
|
||||||
17. **Report Generation** - Professional HTML reports (ReportGenerator, HTMLExporter)
|
|
||||||
|
|
||||||
### Other Notebooks - Standard Pipeline Structure:
|
|
||||||
|
|
||||||
1. **Data Sources** - Multiple ingestion
|
|
||||||
2. **MCP Integration** - (Criminal Network Analysis only)
|
|
||||||
3. **Semantica Agent Setup** - Initialize AgentMemory, create specialized agents
|
|
||||||
4. **Agent-Based Data Gathering** - Autonomous agents gather data
|
|
||||||
5. **Data Parsing** - Parse structured/unstructured data
|
|
||||||
6. **Data Normalization** - Clean and standardize
|
|
||||||
7. **Entity & Relation Extraction** - Extract entities, relationships, events
|
|
||||||
8. **Knowledge Graph Construction** - Build graphs
|
|
||||||
9. **Agent-Based Analysis** - Specialized agents perform parallel analysis
|
|
||||||
10. **Graph Analytics** - Community detection, centrality, connectivity
|
|
||||||
11. **GraphRAG Implementation** - Embeddings, vector store, hybrid search
|
|
||||||
12. **Agent Memory Integration** - Store and retrieve agent context
|
|
||||||
13. **Detailed Analysis** - Reasoning, inference, pattern detection
|
|
||||||
14. **Agent Coordination** - Pipeline module for multi-agent workflow orchestration
|
|
||||||
15. **Visualization** - Network graphs, analytics dashboards, maps
|
|
||||||
16. **Agent-Based Report Generation** - Agents compile comprehensive reports
|
|
||||||
17. **Report Generation** - Professional HTML reports
|
|
||||||
|
|
||||||
### Semantica Agent Implementation:
|
|
||||||
|
|
||||||
- **AgentMemory**: Persistent context storage, memory retrieval, conversation history
|
|
||||||
- **Pipeline Coordination**: PipelineBuilder, ExecutionEngine, ParallelismManager for multi-agent workflows
|
|
||||||
- **Specialized Agents**: Each agent has specific role (data gathering, analysis, reporting)
|
|
||||||
- **Agent Examples**: Code demonstrations of agent workflows with memory integration
|
|
||||||
|
|
||||||
### MCP Integration:
|
|
||||||
|
|
||||||
- **Intelligence Analysis**: MCP browser tools for OSINT, resources for external feeds
|
|
||||||
- **Criminal Network Analysis**: MCP for public records, court databases, API integration
|
|
||||||
- **Agent-MCP Coordination**: Agents use MCP for autonomous data gathering
|
|
||||||
|
|
||||||
### Notebook Structure:
|
|
||||||
|
|
||||||
#### Intelligence Analysis (Orchestrator-Worker Pattern):
|
|
||||||
- Overview with Orchestrator-Worker pattern explanation
|
|
||||||
- Semantica modules used (30+ modules including Orchestrator, Workers, Ontology, Graph Analytics, Hybrid RAG)
|
|
||||||
- **Orchestrator Architecture**: Detailed explanation of orchestrator and worker roles
|
|
||||||
- **Worker Implementation**: Detailed code for each worker (7 workers)
|
|
||||||
- **Ontology Building**: Complete 6-stage ontology generation pipeline demonstration
|
|
||||||
- **Graph Analytics**: All analytics methods (PageRank, Betweenness, Closeness, Eigenvector, Louvain, connectivity, paths)
|
|
||||||
- **Hybrid RAG**: Complete implementation with KG queries + vector search, query orchestration
|
|
||||||
- MCP integration demonstration
|
|
||||||
- Step-by-step implementation with orchestrator coordinating workers
|
|
||||||
- Parallel worker execution examples
|
|
||||||
- Agent memory integration
|
|
||||||
- Best practices for orchestrator-worker pattern
|
|
||||||
- Best practices for agents and MCP
|
|
||||||
- Conclusion with key takeaways
|
|
||||||
|
|
||||||
#### Other Notebooks:
|
|
||||||
- Overview with complete pipeline description
|
|
||||||
- Semantica modules used (20+ modules including AgentMemory, Pipeline)
|
|
||||||
- Agent Architecture explanation
|
|
||||||
- MCP integration demonstration (Criminal Network Analysis)
|
|
||||||
- Step-by-step implementation with agent workflows
|
|
||||||
- Agent memory integration examples
|
|
||||||
- Multi-agent pipeline orchestration
|
|
||||||
- Best practices for agents and MCP
|
|
||||||
- Conclusion with key takeaways
|
|
||||||
|
|
||||||
## Key Implementation Details for Orchestrator-Worker Pattern:
|
|
||||||
|
|
||||||
### Orchestrator Code Example:
|
|
||||||
```python
|
|
||||||
from semantica.pipeline import PipelineBuilder, ExecutionEngine, ParallelismManager
|
|
||||||
from semantica.ontology import OntologyGenerator
|
|
||||||
from semantica.kg import GraphBuilder, GraphAnalyzer
|
|
||||||
from semantica.vector_store import VectorStore, HybridSearch
|
|
||||||
from semantica.context import AgentMemory
|
|
||||||
|
|
||||||
# Initialize orchestrator
|
|
||||||
orchestrator = ExecutionEngine()
|
|
||||||
parallelism_manager = ParallelismManager(max_workers=7)
|
|
||||||
|
|
||||||
# Define workers
|
|
||||||
def data_ingestion_worker(sources):
|
|
||||||
# Worker 1: Multi-source data ingestion
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ontology_building_worker(entities, relationships):
|
|
||||||
# Worker 2: Complete 6-stage ontology generation
|
|
||||||
ontology_gen = OntologyGenerator()
|
|
||||||
ontology = ontology_gen.generate_ontology({"entities": entities, "relationships": relationships})
|
|
||||||
return ontology
|
|
||||||
|
|
||||||
def graph_construction_worker(entities, relationships):
|
|
||||||
# Worker 3: Build knowledge graph
|
|
||||||
graph_builder = GraphBuilder()
|
|
||||||
kg = graph_builder.build(entities, relationships)
|
|
||||||
return kg
|
|
||||||
|
|
||||||
def graph_analytics_worker(kg):
|
|
||||||
# Worker 4: All graph analytics
|
|
||||||
analyzer = GraphAnalyzer()
|
|
||||||
pagerank = analyzer.compute_centrality(kg, method="pagerank")
|
|
||||||
betweenness = analyzer.compute_centrality(kg, method="betweenness")
|
|
||||||
communities = analyzer.detect_communities(kg, method="louvain")
|
|
||||||
# ... all analytics
|
|
||||||
return {"pagerank": pagerank, "betweenness": betweenness, "communities": communities}
|
|
||||||
|
|
||||||
def hybrid_rag_worker(kg, vector_store):
|
|
||||||
# Worker 5: Hybrid RAG with KG and vector store
|
|
||||||
hybrid_search = HybridSearch(vector_store=vector_store, knowledge_graph=kg)
|
|
||||||
# Query orchestration
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Build pipeline with workers
|
|
||||||
pipeline = PipelineBuilder() \
|
|
||||||
.add_step("data_ingestion", "custom", func=data_ingestion_worker) \
|
|
||||||
.add_step("ontology_building", "custom", func=ontology_building_worker) \
|
|
||||||
.add_step("graph_construction", "custom", func=graph_construction_worker) \
|
|
||||||
.add_step("graph_analytics", "custom", func=graph_analytics_worker) \
|
|
||||||
.add_step("hybrid_rag", "custom", func=hybrid_rag_worker) \
|
|
||||||
.build()
|
|
||||||
|
|
||||||
# Execute with parallel workers
|
|
||||||
result = orchestrator.execute_pipeline(pipeline, parallel=True, max_workers=7)
|
|
||||||
```
|
|
||||||
|
|
||||||
Each notebook demonstrates the full journey from raw data sources through autonomous agent workflows (or orchestrator-worker pattern) and GraphRAG to actionable intelligence.
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# PR: Context Module Testing & Validation
|
|
||||||
|
|
||||||
## Description
|
|
||||||
This PR adds comprehensive testing and validation for the **Context Engineering Module** (`semantica.context`). It includes unit tests for core components, verification of notebook examples, and a critical bug fix in the deduplication module.
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
### 1. New Unit Tests (`tests/context/`)
|
|
||||||
Added `tests/context/test_context.py` covering:
|
|
||||||
- **AgentContext**: End-to-end storage and retrieval (RAG & GraphRAG).
|
|
||||||
- **AgentMemory**: Hierarchical memory management (short-term buffer vs. long-term vector store) and retention policies.
|
|
||||||
- **ContextGraph**: Node/edge addition and neighbor traversal.
|
|
||||||
- **EntityLinker**: URI assignment and entity linking logic.
|
|
||||||
- **ContextRetriever**: Hybrid retrieval strategies (Vector + Graph).
|
|
||||||
|
|
||||||
### 2. Notebook Verification
|
|
||||||
Verified functionality of the following notebooks by converting them to test scripts:
|
|
||||||
- `19_Context_Module.ipynb`: Verified high-level interface, token limits, and graph construction.
|
|
||||||
- `11_Advanced_Context_Engineering.ipynb`: Verified custom memory pruning, hybrid tuning, and custom graph builders.
|
|
||||||
|
|
||||||
### 3. Bug Fixes
|
|
||||||
- **`semantica/deduplication/merge_strategy.py`**: Fixed a `NameError` caused by a missing `Tuple` import. This was discovered during global import validation.
|
|
||||||
|
|
||||||
### 4. Verification
|
|
||||||
- All new tests passed.
|
|
||||||
- Global import check confirmed no other hidden dependency issues.
|
|
||||||
- Integration test `verify_context_sync.py` passed, confirming correct synchronization between memory, graph, and vector store.
|
|
||||||
|
|
||||||
## Testing Instructions
|
|
||||||
Run the new tests with:
|
|
||||||
```bash
|
|
||||||
python -m unittest tests/context/test_context.py
|
|
||||||
```
|
|
||||||
+41
-28
@@ -1,34 +1,47 @@
|
|||||||
# Enhanced Export Module Testing, Bug Fixes & Notebook Updates
|
# Refactor Semantic Extract Module to Class-Based Interfaces
|
||||||
|
|
||||||
## Summary
|
## 📝 Summary
|
||||||
This PR significantly hardens the `semantica.export` module by adding comprehensive unit tests, fixing critical bugs in export wrappers and logic, and updating documentation and cookbooks to match current API signatures.
|
This PR refactors the Semantic Extract module to promote a cleaner, object-oriented API for Entity, Relation, and Triple extraction. It standardizes the usage around `NERExtractor`, `RelationExtractor`, and `TripleExtractor` classes, replacing the previous low-level `get_entity_method` factory functions in user-facing code.
|
||||||
|
|
||||||
## Key Changes
|
## 🚀 Motivation
|
||||||
|
The previous API relied heavily on factory functions (`get_entity_method("pattern")`), which made discovery and configuration difficult for users. The new class-based approach:
|
||||||
|
- Improves code readability and IDE auto-completion.
|
||||||
|
- Provides a consistent interface (`extractor.extract()`) across all extraction tasks.
|
||||||
|
- Aligns the documentation and cookbooks with the actual best practices.
|
||||||
|
|
||||||
### 1. Bug Fixes & Logic Improvements
|
## 🔍 Key Changes
|
||||||
- **`semantica/export/methods.py`**:
|
|
||||||
- Fixed `export_yaml(method="schema")` to correctly call `export_ontology_schema` and handle file writing (previously failed as the underlying method returns a string).
|
|
||||||
- Added safeguards to all convenience functions (`export_rdf`, `export_json`, etc.) to prevent infinite recursion if the registry returns the wrapper function itself.
|
|
||||||
- **`semantica/kg/graph_builder.py`**: Fixed a critical bug where `ConflictDetector` was receiving the entire graph dictionary instead of the entity list.
|
|
||||||
- **`semantica/export/rdf_exporter.py`**: Fixed `export_to_rdf` to correctly return serialized data for all formats.
|
|
||||||
|
|
||||||
### 2. Comprehensive Testing (`tests/`)
|
### 1. API Refactoring
|
||||||
- **`tests/test_export_module.py`**: A full suite of unit tests covering all 11 export classes (`JSON`, `CSV`, `RDF`, `GraphML`, `YAML`, `OWL`, `Vector`, `LPG`, etc.).
|
- **Standardized Classes**: Promoted `NERExtractor`, `RelationExtractor`, and `TripleExtractor` as the primary entry points.
|
||||||
- **`tests/test_export_methods_wrapper.py`**: Added specific tests for convenience wrapper functions in `methods.py`, verifying the fix for schema export.
|
- **Method Aliases**: Added `extract()` aliases to `extract_entities()` and `extract_relations()` for a uniform API surface.
|
||||||
- **`tests/test_notebook_15_export.py`** & **`tests/test_notebooks_simulation.py`**: Simulation tests that replicate cookbook logic to ensure end-to-end functionality.
|
- **Configuration**: Unified configuration passing via class constructors.
|
||||||
|
|
||||||
### 3. Documentation & Notebook Updates
|
### 2. Documentation Updates (`docs/reference/semantic_extract.md`)
|
||||||
- **`docs/reference/export.md`** & **`semantica/export/export_usage.md`**: Updated to correctly document `YAMLSchemaExporter.export_ontology_schema` instead of the deprecated `export` method.
|
- Added missing documentation for **Semantic Networks**, **Coreference Resolution**, and **LLM Enhancement**.
|
||||||
- **Cookbooks** (`15_Export.ipynb`, `05_Multi_Format_Export.ipynb`):
|
- Updated all code examples to use the new class-based API.
|
||||||
- Updated `GraphBuilder.build()` calls to pass combined lists (fixing API mismatch).
|
- Added a "Semantic Networks" card to the overview for better discoverability.
|
||||||
- Corrected `YAMLSchemaExporter` usage.
|
|
||||||
- Fixed `VectorExporter` data preparation.
|
|
||||||
- Adjusted `CSVExporter` paths.
|
|
||||||
|
|
||||||
## Verification
|
### 3. Cookbook Updates
|
||||||
All tests passed successfully:
|
- **`05_Entity_Extraction.ipynb`**: Refactored to use `NERExtractor` for Pattern, Regex, ML, and LLM examples.
|
||||||
```bash
|
- **`06_Relation_Extraction.ipynb`**: Refactored to use `RelationExtractor` for dependency and pattern-based examples.
|
||||||
$ pytest tests/test_export_module.py tests/test_notebooks_simulation.py tests/test_notebook_15_export.py tests/test_export_methods_wrapper.py
|
- **`11_Chunking_and_Splitting.ipynb`**: Updated to use consistent method names (`ner_method="ml"`).
|
||||||
...
|
|
||||||
13 passed in 3.82s
|
### 4. Split Module Improvements
|
||||||
```
|
- **Method Aliasing**: Added aliases in `methods.py` to support "spacy" (mapping to "ml") and "ml" (mapping to "dependency" for relations), improving robustness and user experience.
|
||||||
|
- **Robustness**: Verified `EntityAwareChunker` and `RelationAwareChunker` fallback mechanisms.
|
||||||
|
|
||||||
|
### 5. Testing
|
||||||
|
- Added `tests/test_ner_configurations.py` to verify all NER method configurations.
|
||||||
|
- Added `tests/test_notebooks_verification.py` to ensure notebook examples run correctly.
|
||||||
|
- Added `tests/test_semantic_extract_deepdive.py` covering relation and triple extraction scenarios.
|
||||||
|
|
||||||
|
## 🧪 Verification
|
||||||
|
- [x] **Unit Tests**: All new tests pass, verifying correct instantiation and execution of extractors.
|
||||||
|
- [x] **Notebooks**: Verified that the updated cookbooks run without errors.
|
||||||
|
- [x] **Documentation**: previewed `semantic_extract.md` to ensure correct rendering of new sections.
|
||||||
|
|
||||||
|
## ✅ Checklist
|
||||||
|
- [x] Code follows the project's coding standards.
|
||||||
|
- [x] Documentation has been updated to reflect the changes.
|
||||||
|
- [x] Tests have been added to cover the new functionality.
|
||||||
|
- [x] Cookbooks have been updated and verified.
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# feat: Knowledge Engineering Module Enhancements and Testing
|
|
||||||
|
|
||||||
## 📝 Description
|
|
||||||
This PR significantly enhances the stability, test coverage, and documentation of the `knowledge-engineering` module and related components (`ontology`, `visualization`, `conflicts`, etc.). It addresses critical bugs preventing pipeline execution and establishes a comprehensive testing baseline.
|
|
||||||
|
|
||||||
## 🚀 Key Changes
|
|
||||||
|
|
||||||
### 1. 🧪 Comprehensive Unit Testing
|
|
||||||
Added and verified over **100+ new unit tests** across multiple modules to ensure robustness:
|
|
||||||
- **Knowledge Graph (`semantica.kg`)**:
|
|
||||||
- `test_core_components.py`: Validates `GraphBuilder`, `EntityResolver`, `GraphValidator`, `ProvenanceTracker`.
|
|
||||||
- `test_algorithms.py`: Covers `CentralityCalculator`, `CommunityDetector`, `ConnectivityAnalyzer`.
|
|
||||||
- **Ontology (`semantica.ontology`)**:
|
|
||||||
- `test_ontology_classes.py`: Tests core ontology generation logic.
|
|
||||||
- `test_ontology_advanced.py`: Validates validation, metrics, and complex class relationships.
|
|
||||||
- **Visualization (`semantica.visualization`)**:
|
|
||||||
- Added tests for `GraphVisualizer` and interactive plotting components.
|
|
||||||
- **Data Handling**:
|
|
||||||
- `semantica.split`: Added `test_splitter.py`.
|
|
||||||
- `semantica.parse`: Added `test_parser.py` (with fixes for `pathlib` mocking).
|
|
||||||
- `semantica.vector_store` & `semantica.triple_store`: Enhanced with full CRUD operation tests.
|
|
||||||
- **Utilities**:
|
|
||||||
- `semantica.seed`: Validated seed management.
|
|
||||||
- `semantica.utils`: Verified shared utility functions.
|
|
||||||
|
|
||||||
### 2. 🐛 Bug Fixes & Stability Improvements
|
|
||||||
- **Conflict Resolution**: Implemented a placeholder `resolve_conflicts` method in `ConflictDetector` to unblock pipeline execution failures where this method was missing.
|
|
||||||
- **Inference Engine**: Fixed `TypeError: unhashable type: 'dict'` by handling unhashable facts in `InferenceEngine`.
|
|
||||||
- **Circular Imports**: Resolved circular dependency issues in `semantic_extract` by deferring imports.
|
|
||||||
- **Test Infrastructure**:
|
|
||||||
- Fixed `test_cookbook_integration.py` by mocking MCP server connections (`httpx`/`requests`) to prevent WinError 10061.
|
|
||||||
- Fixed `pathlib.Path` mocking issues in parser tests.
|
|
||||||
|
|
||||||
### 3. 📚 Documentation Updates
|
|
||||||
- **`semantica/kg/kg_usage.md`**: Updated usage guide to reflect current capabilities and configuration options.
|
|
||||||
- **`semantica/conflicts/conflicts_usage.md`**: Added documentation for the `resolve_conflicts` convenience method.
|
|
||||||
|
|
||||||
## ✅ Verification
|
|
||||||
- All new and existing unit tests pass.
|
|
||||||
- `python -m unittest discover tests/kg` runs successfully.
|
|
||||||
- Pipeline execution no longer crashes due to missing methods or unhashable types.
|
|
||||||
|
|
||||||
## 📦 Related Issues
|
|
||||||
- Fixes pipeline crashes during conflict resolution.
|
|
||||||
- Addresses missing test coverage for core KG components.
|
|
||||||
-206
@@ -1,206 +0,0 @@
|
|||||||
# Add Intelligence Cookbook Notebooks with MCP and Semantica Agents
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Add comprehensive intelligence-focused notebooks to `cookbook/use_cases/intelligence/` with complete end-to-end pipelines covering data ingestion (including MCP integration), knowledge graph construction, GraphRAG implementation, **Semantica agent-based workflows**, and detailed analysis. Update documentation in `docs/cookbook.md` and `docs/use-cases.md`.
|
|
||||||
|
|
||||||
## New Notebooks to Create
|
|
||||||
|
|
||||||
### 1. Criminal Network Analysis (`Criminal_Network_Analysis.ipynb`)
|
|
||||||
Complete pipeline from data sources to GraphRAG with **agent-based workflows**:
|
|
||||||
- **Data Sources**: Ingest from police reports, court records, surveillance data, communication logs
|
|
||||||
- **MCP Integration**: Utilize MCP for accessing public records databases, court records APIs, and real-time data streams
|
|
||||||
- **Semantica Agents**:
|
|
||||||
- **Data Gathering Agent**: Autonomous agent using AgentMemory to gather and track data from multiple sources
|
|
||||||
- **Network Analysis Agent**: Specialized agent for graph analytics and community detection
|
|
||||||
- **Pattern Detection Agent**: Agent for identifying suspicious patterns and relationships
|
|
||||||
- **Report Generation Agent**: Agent for compiling intelligence reports
|
|
||||||
- **Agent Coordination**: Use Pipeline module (PipelineBuilder, ExecutionEngine, ParallelismManager) to coordinate parallel agent workflows
|
|
||||||
- **Agent Memory**: Use AgentMemory for persistent context across agent interactions
|
|
||||||
- **Parsing**: Parse structured/unstructured documents, JSON, CSV, PDFs
|
|
||||||
- **Extraction**: Extract suspects, organizations, locations, events, relationships
|
|
||||||
- **Knowledge Graph**: Build criminal network graph with temporal relationships
|
|
||||||
- **Graph Analytics**: Community detection, centrality measures, key player identification
|
|
||||||
- **GraphRAG**: Vector store, hybrid search, context retrieval for intelligence queries
|
|
||||||
- **Detailed Analysis**: Pattern detection, network structure analysis, threat assessment
|
|
||||||
- **Visualization**: Network graphs, community visualization, centrality rankings
|
|
||||||
- **Reporting**: Generate intelligence reports on criminal structures
|
|
||||||
|
|
||||||
### 2. Law Enforcement and Forensics (`Law_Enforcement_Forensics.ipynb`)
|
|
||||||
Complete forensic analysis pipeline with **agent-based workflows**:
|
|
||||||
- **Data Sources**: Case files, evidence logs, witness statements, forensic reports, crime scene data
|
|
||||||
- **Semantica Agents**:
|
|
||||||
- **Evidence Collection Agent**: Autonomous agent for gathering and organizing evidence
|
|
||||||
- **Timeline Analysis Agent**: Agent for building temporal case timelines
|
|
||||||
- **Cross-Case Correlation Agent**: Agent for finding connections across multiple cases
|
|
||||||
- **Forensic Report Agent**: Agent for generating comprehensive forensic reports
|
|
||||||
- **Agent Coordination**: Multi-agent pipeline for parallel evidence processing
|
|
||||||
- **Agent Memory**: Persistent memory for case context and evidence chains
|
|
||||||
- **Parsing**: Parse PDFs, structured reports, evidence databases, temporal logs
|
|
||||||
- **Extraction**: Extract entities (persons, locations, evidence, events), relationships, timelines
|
|
||||||
- **Knowledge Graph**: Build temporal knowledge graph for case timelines and evidence correlation
|
|
||||||
- **Graph Analytics**: Timeline analysis, evidence correlation, pattern detection across cases
|
|
||||||
- **GraphRAG**: Semantic search across case files, evidence retrieval, context-aware queries
|
|
||||||
- **Detailed Analysis**: Cross-case correlation, evidence chain analysis, suspect identification
|
|
||||||
- **Visualization**: Timeline visualization, evidence networks, case correlation graphs
|
|
||||||
- **Reporting**: Generate forensic analysis reports with evidence chains
|
|
||||||
|
|
||||||
### 3. Intelligence Analysis (`Intelligence_Analysis.ipynb`)
|
|
||||||
Comprehensive intelligence analysis with **agent-based workflows**:
|
|
||||||
- **Data Sources**: OSINT feeds, threat intelligence, social media, news, public records, geospatial data
|
|
||||||
- **MCP Integration**: Utilize MCP for real-time data fetching, web scraping, API integration, external database access, and browser automation for OSINT gathering
|
|
||||||
- **Semantica Agents**:
|
|
||||||
- **OSINT Gathering Agent**: Autonomous agent using MCP browser tools for web scraping and OSINT collection
|
|
||||||
- **Threat Assessment Agent**: Specialized agent for threat analysis and risk scoring
|
|
||||||
- **Geospatial Intelligence Agent**: Agent for location-based tracking and geographic analysis
|
|
||||||
- **Multi-Source Fusion Agent**: Agent for correlating intelligence from multiple sources
|
|
||||||
- **Intelligence Report Agent**: Agent for generating comprehensive threat intelligence reports
|
|
||||||
- **Agent Coordination**: Complex multi-agent pipeline with parallel execution for intelligence gathering
|
|
||||||
- **Agent Memory**: Persistent memory for threat context, entity tracking, and intelligence history
|
|
||||||
- **Parsing**: Multi-format parsing (RSS feeds, JSON, XML, web scraping, geospatial formats)
|
|
||||||
- **Extraction**: Extract threat actors, locations, events, relationships, temporal patterns
|
|
||||||
- **Knowledge Graph**: Build multi-source intelligence graph with geospatial and temporal dimensions
|
|
||||||
- **Graph Analytics**: Threat assessment, risk scoring, entity relationship mapping, pattern detection
|
|
||||||
- **GraphRAG**: Multi-source intelligence fusion, hybrid search, contextual threat queries
|
|
||||||
- **Detailed Analysis**:
|
|
||||||
- Multi-source intelligence fusion and correlation
|
|
||||||
- Threat assessment and risk analysis
|
|
||||||
- Geospatial intelligence with location tracking
|
|
||||||
- Temporal threat evolution analysis
|
|
||||||
- **Visualization**: Geographic network maps, threat timelines, relationship networks
|
|
||||||
- **Reporting**: Generate comprehensive threat intelligence reports
|
|
||||||
|
|
||||||
## Files to Create/Modify
|
|
||||||
|
|
||||||
### New Notebooks (in `cookbook/use_cases/intelligence/`)
|
|
||||||
- `Criminal_Network_Analysis.ipynb`
|
|
||||||
- `Law_Enforcement_Forensics.ipynb`
|
|
||||||
- `Intelligence_Analysis.ipynb`
|
|
||||||
|
|
||||||
### Documentation Updates
|
|
||||||
- `docs/cookbook.md` - Add new notebooks to Intelligence section
|
|
||||||
- `docs/use-cases.md` - Add new use case cards for criminal networks and law enforcement
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Complete Pipeline Structure (All Notebooks):
|
|
||||||
1. **Data Sources** - Multiple ingestion sources (FileIngestor, DBIngestor, WebIngestor, StreamIngestor, FeedIngestor)
|
|
||||||
2. **MCP Integration** - Utilize MCP servers for external data access, real-time feeds, API integration, web scraping, and browser automation (in Intelligence Analysis and Criminal Network Analysis notebooks)
|
|
||||||
3. **Semantica Agent Setup** - Initialize AgentMemory, create specialized agents, set up agent coordination
|
|
||||||
4. **Agent-Based Data Gathering** - Autonomous agents gather data using MCP and Semantica ingestors
|
|
||||||
5. **Data Parsing** - Parse structured/unstructured data (JSONParser, XMLParser, CSVParser, DocumentParser, StructuredDataParser)
|
|
||||||
6. **Data Normalization** - Clean and standardize (TextNormalizer, DataNormalizer)
|
|
||||||
7. **Entity & Relation Extraction** - Extract entities, relationships, events (NERExtractor, RelationExtractor, TripleExtractor, EventDetector)
|
|
||||||
8. **Knowledge Graph Construction** - Build graphs (GraphBuilder, TemporalGraphQuery)
|
|
||||||
9. **Agent-Based Analysis** - Specialized agents perform parallel analysis tasks
|
|
||||||
10. **Graph Analytics** - Community detection, centrality, connectivity (GraphAnalyzer, ConnectivityAnalyzer, CentralityCalculator)
|
|
||||||
11. **GraphRAG Implementation** - Embeddings, vector store, hybrid search, context retrieval (EmbeddingGenerator, VectorStore, HybridSearch, ContextRetriever)
|
|
||||||
12. **Agent Memory Integration** - Store and retrieve agent context using AgentMemory
|
|
||||||
13. **Detailed Analysis** - Reasoning, inference, pattern detection (InferenceEngine, RuleManager, ExplanationGenerator)
|
|
||||||
14. **Agent Coordination** - Use Pipeline module for multi-agent workflow orchestration
|
|
||||||
15. **Visualization** - Network graphs, analytics dashboards, geographic maps (KGVisualizer, AnalyticsVisualizer, TemporalVisualizer)
|
|
||||||
16. **Agent-Based Report Generation** - Agents compile and generate professional reports
|
|
||||||
17. **Report Generation** - Professional HTML reports (ReportGenerator, HTMLExporter)
|
|
||||||
|
|
||||||
### Semantica Agent Implementation Details:
|
|
||||||
|
|
||||||
#### AgentMemory Usage:
|
|
||||||
- **Persistent Context**: Store agent interactions, decisions, and findings
|
|
||||||
- **Memory Retrieval**: Retrieve relevant context for agent decision-making
|
|
||||||
- **Conversation History**: Track agent conversations and analysis sessions
|
|
||||||
- **Context Accumulation**: Build up intelligence context over time
|
|
||||||
|
|
||||||
#### Pipeline Agent Coordination:
|
|
||||||
- **PipelineBuilder**: Define multi-agent workflows
|
|
||||||
- **ExecutionEngine**: Execute agent pipelines with error handling
|
|
||||||
- **ParallelismManager**: Run agents in parallel for efficiency
|
|
||||||
- **Specialized Agents**: Each agent has a specific role (data gathering, analysis, reporting)
|
|
||||||
|
|
||||||
#### Agent Workflow Examples:
|
|
||||||
```python
|
|
||||||
# Example: Multi-agent intelligence gathering
|
|
||||||
from semantica.context import AgentMemory
|
|
||||||
from semantica.pipeline import PipelineBuilder, ExecutionEngine, ParallelismManager
|
|
||||||
|
|
||||||
# Initialize agent memory
|
|
||||||
agent_memory = AgentMemory(vector_store=vs, knowledge_graph=kg)
|
|
||||||
|
|
||||||
# Define specialized agents
|
|
||||||
def osint_gathering_agent(query, memory):
|
|
||||||
"""Autonomous OSINT gathering agent"""
|
|
||||||
# Use MCP for web scraping
|
|
||||||
# Store findings in agent memory
|
|
||||||
findings = gather_osint(query)
|
|
||||||
memory.store(f"OSINT findings: {findings}", metadata={"agent": "osint", "query": query})
|
|
||||||
return findings
|
|
||||||
|
|
||||||
def threat_assessment_agent(intel_data, memory):
|
|
||||||
"""Threat assessment agent"""
|
|
||||||
# Retrieve relevant context from memory
|
|
||||||
context = memory.retrieve("threat patterns", max_results=10)
|
|
||||||
# Perform threat analysis
|
|
||||||
assessment = analyze_threats(intel_data, context)
|
|
||||||
memory.store(f"Threat assessment: {assessment}", metadata={"agent": "threat"})
|
|
||||||
return assessment
|
|
||||||
|
|
||||||
# Build multi-agent pipeline
|
|
||||||
pipeline = PipelineBuilder() \
|
|
||||||
.add_step("osint_gathering", "custom", func=osint_gathering_agent, args=(query, agent_memory)) \
|
|
||||||
.add_step("threat_assessment", "custom", func=threat_assessment_agent, args=(intel_data, agent_memory)) \
|
|
||||||
.build()
|
|
||||||
|
|
||||||
# Execute with parallel agents
|
|
||||||
engine = ExecutionEngine()
|
|
||||||
result = engine.execute_pipeline(pipeline, parallel=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
### MCP Integration Details:
|
|
||||||
- **Intelligence Analysis Notebook**:
|
|
||||||
- Use MCP browser tools for web scraping and OSINT gathering
|
|
||||||
- Use MCP resources for accessing external intelligence feeds
|
|
||||||
- Demonstrate real-time data fetching via MCP
|
|
||||||
- Agents use MCP for autonomous data gathering
|
|
||||||
- **Criminal Network Analysis Notebook**:
|
|
||||||
- Use MCP for accessing public records and court databases
|
|
||||||
- Demonstrate API integration via MCP
|
|
||||||
- Show real-time data stream processing
|
|
||||||
- Agents coordinate MCP-based data gathering
|
|
||||||
|
|
||||||
### Notebook Structure:
|
|
||||||
- Overview with complete pipeline description
|
|
||||||
- Semantica modules used (20+ modules including AgentMemory, Pipeline)
|
|
||||||
- **Agent Architecture**: Explanation of agent roles and coordination
|
|
||||||
- MCP integration demonstration (for Intelligence Analysis and Criminal Network Analysis)
|
|
||||||
- Step-by-step implementation:
|
|
||||||
- **Agent Setup**: Initialize AgentMemory and create specialized agents
|
|
||||||
- Data ingestion from multiple sources (including MCP resources)
|
|
||||||
- **Agent-Based Data Gathering**: Autonomous agents gather data
|
|
||||||
- MCP-based external data fetching and API integration
|
|
||||||
- Parsing and normalization
|
|
||||||
- Entity and relation extraction
|
|
||||||
- Knowledge graph construction
|
|
||||||
- **Agent-Based Analysis**: Parallel agent workflows for analysis
|
|
||||||
- Graph analytics and pattern detection
|
|
||||||
- **Agent Memory Integration**: Store and retrieve agent context
|
|
||||||
- GraphRAG setup and query examples
|
|
||||||
- **Agent Coordination**: Multi-agent pipeline orchestration
|
|
||||||
- Detailed analysis with insights
|
|
||||||
- Visualization examples
|
|
||||||
- **Agent-Based Report Generation**: Agents compile reports
|
|
||||||
- Report generation
|
|
||||||
- Best practices and deployment recommendations
|
|
||||||
- **Agent Best Practices**: Agent memory management, coordination patterns
|
|
||||||
- MCP integration best practices
|
|
||||||
- Conclusion with key takeaways
|
|
||||||
|
|
||||||
Each notebook will be comprehensive, demonstrating the full journey from raw data sources (including MCP-enabled external sources) through **autonomous agent workflows** and GraphRAG to actionable intelligence and detailed analysis.
|
|
||||||
|
|
||||||
## Key Agent Features to Highlight:
|
|
||||||
|
|
||||||
1. **Autonomous Data Gathering**: Agents independently gather data from multiple sources
|
|
||||||
2. **Persistent Memory**: AgentMemory maintains context across sessions
|
|
||||||
3. **Parallel Coordination**: Multiple agents work simultaneously on different tasks
|
|
||||||
4. **Specialized Roles**: Each agent has a specific expertise area
|
|
||||||
5. **Context-Aware Analysis**: Agents use memory to make informed decisions
|
|
||||||
6. **Coordinated Workflows**: Pipeline module orchestrates complex multi-agent systems
|
|
||||||
7. **Intelligent Reporting**: Agents compile findings into comprehensive reports
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -204,7 +204,7 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"from semantica.semantic_extract.methods import get_entity_method\n",
|
"from semantica.semantic_extract import NERExtractor\n",
|
||||||
"\n",
|
"\n",
|
||||||
"sample_text = \"Apple Inc. was founded by Steve Jobs in Cupertino, California in 1976.\"\n",
|
"sample_text = \"Apple Inc. was founded by Steve Jobs in Cupertino, California in 1976.\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -219,8 +219,8 @@
|
|||||||
" print(f\"\\n Method: {method_name.upper()}\")\n",
|
" print(f\"\\n Method: {method_name.upper()}\")\n",
|
||||||
" print(\"-\" * 40)\n",
|
" print(\"-\" * 40)\n",
|
||||||
" \n",
|
" \n",
|
||||||
" method = get_entity_method(method_name)\n",
|
" extractor = NERExtractor(method=method_name)\n",
|
||||||
" entities = method(sample_text)\n",
|
" entities = extractor.extract(sample_text)\n",
|
||||||
" \n",
|
" \n",
|
||||||
" print(f\"Found {len(entities)} entities:\")\n",
|
" print(f\"Found {len(entities)} entities:\")\n",
|
||||||
" for entity in entities[:5]: # Show first 5\n",
|
" for entity in entities[:5]: # Show first 5\n",
|
||||||
@@ -638,4 +638,4 @@
|
|||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
"nbformat_minor": 2
|
"nbformat_minor": 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"from semantica.semantic_extract.methods import get_relation_method\n",
|
"from semantica.semantic_extract import RelationExtractor\n",
|
||||||
"\n",
|
"\n",
|
||||||
"sample_text = \"Apple Inc. was founded by Steve Jobs in Cupertino, California.\"\n",
|
"sample_text = \"Apple Inc. was founded by Steve Jobs in Cupertino, California.\"\n",
|
||||||
"sample_entities = ner_extractor.extract(sample_text)\n",
|
"sample_entities = ner_extractor.extract(sample_text)\n",
|
||||||
@@ -196,8 +196,8 @@
|
|||||||
" print(f\"\\n Method: {method_name.upper()}\")\n",
|
" print(f\"\\n Method: {method_name.upper()}\")\n",
|
||||||
" print(\"-\" * 40)\n",
|
" print(\"-\" * 40)\n",
|
||||||
" \n",
|
" \n",
|
||||||
" method = get_relation_method(method_name)\n",
|
" extractor = RelationExtractor(method=method_name)\n",
|
||||||
" relations = method(sample_text, sample_entities)\n",
|
" relations = extractor.extract(sample_text, sample_entities)\n",
|
||||||
" \n",
|
" \n",
|
||||||
" print(f\"Found {len(relations)} relations:\")\n",
|
" print(f\"Found {len(relations)} relations:\")\n",
|
||||||
" for rel in relations[:3]: # Show first 3\n",
|
" for rel in relations[:3]: # Show first 3\n",
|
||||||
@@ -690,4 +690,4 @@
|
|||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
"nbformat_minor": 2
|
"nbformat_minor": 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
"entity_chunker = EntityAwareChunker(\n",
|
"entity_chunker = EntityAwareChunker(\n",
|
||||||
" chunk_size=200,\n",
|
" chunk_size=200,\n",
|
||||||
" chunk_overlap=50,\n",
|
" chunk_overlap=50,\n",
|
||||||
" ner_method=\"spacy\", # or \"llm\" for better accuracy\n",
|
" ner_method=\"ml\", # \"ml\" (spaCy), \"pattern\", or \"llm\"\n",
|
||||||
" preserve_entities=True\n",
|
" preserve_entities=True\n",
|
||||||
")\n",
|
")\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
|||||||
Binary file not shown.
@@ -1,17 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import os
|
|
||||||
|
|
||||||
def run_git_cmd(cmd):
|
|
||||||
print(f"--- Running: {cmd} ---")
|
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, shell=True, check=False, capture_output=True, text=True)
|
|
||||||
print("STDOUT:", result.stdout)
|
|
||||||
print("STDERR:", result.stderr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error running {cmd}: {e}")
|
|
||||||
|
|
||||||
print(f"CWD: {os.getcwd()}")
|
|
||||||
run_git_cmd("git status")
|
|
||||||
run_git_cmd("git branch -v")
|
|
||||||
run_git_cmd("git remote -v")
|
|
||||||
run_git_cmd("git push origin knowledge-engineering")
|
|
||||||
Binary file not shown.
@@ -44,6 +44,12 @@
|
|||||||
|
|
||||||
Use LLMs to improve extraction quality and handle complex schemas
|
Use LLMs to improve extraction quality and handle complex schemas
|
||||||
|
|
||||||
|
- :material-graph:{ .lg .middle } **Semantic Networks**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Build structured networks with nodes and edges from text
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
!!! tip "When to Use"
|
!!! tip "When to Use"
|
||||||
@@ -119,6 +125,49 @@ ner = NamedEntityRecognizer(
|
|||||||
entities = ner.extract_entities("Apple Inc. was founded in 1976.")
|
entities = ner.extract_entities("Apple Inc. was founded in 1976.")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### NERExtractor
|
||||||
|
|
||||||
|
Core entity extraction implementation used by notebooks and lower-level integrations.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `method` | str or list | `"ml"` | Method(s): "ml", "llm", "pattern", "regex", "huggingface" |
|
||||||
|
| `**config` | dict | `{}` | Method-specific config (e.g., `model`, `provider`) |
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `extract(text)` | Alias for `extract_entities`. Get list of entities. |
|
||||||
|
| `extract_entities(text)` | Get list of entities |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from semantica.semantic_extract import NERExtractor
|
||||||
|
|
||||||
|
# 1. ML (spaCy) - Default
|
||||||
|
extractor = NERExtractor(method="ml", model="en_core_web_trf")
|
||||||
|
entities = extractor.extract("Elon Musk leads SpaceX.")
|
||||||
|
|
||||||
|
# 2. LLM (OpenAI/Gemini/etc)
|
||||||
|
extractor = NERExtractor(
|
||||||
|
method="llm",
|
||||||
|
provider="openai",
|
||||||
|
model="gpt-4",
|
||||||
|
temperature=0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Regex with custom patterns
|
||||||
|
patterns = {"CODE": r"[A-Z]{3}-\d{3}"}
|
||||||
|
extractor = NERExtractor(method="regex", patterns=patterns)
|
||||||
|
|
||||||
|
# 4. Ensemble (Multiple methods)
|
||||||
|
extractor = NERExtractor(method=["ml", "llm"], ensemble_voting=True)
|
||||||
|
```
|
||||||
|
|
||||||
### RelationExtractor
|
### RelationExtractor
|
||||||
|
|
||||||
Extracts relationships between entities.
|
Extracts relationships between entities.
|
||||||
@@ -136,6 +185,7 @@ Extracts relationships between entities.
|
|||||||
|
|
||||||
| Method | Description |
|
| Method | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
|
| `extract(text, entities)` | Alias for `extract_relations`. Find links. |
|
||||||
| `extract_relations(text, entities)` | Find links |
|
| `extract_relations(text, entities)` | Find links |
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
@@ -150,7 +200,7 @@ entities = ner.extract_entities(text)
|
|||||||
|
|
||||||
# Basic relation extraction
|
# Basic relation extraction
|
||||||
rel_extractor = RelationExtractor()
|
rel_extractor = RelationExtractor()
|
||||||
relations = rel_extractor.extract_relations(text, entities=entities)
|
relations = rel_extractor.extract(text, entities=entities)
|
||||||
# [Relation(source="Elon Musk", target="SpaceX", type="founded")]
|
# [Relation(source="Elon Musk", target="SpaceX", type="founded")]
|
||||||
|
|
||||||
# With configuration
|
# With configuration
|
||||||
@@ -159,7 +209,39 @@ rel_extractor = RelationExtractor(
|
|||||||
confidence_threshold=0.7,
|
confidence_threshold=0.7,
|
||||||
bidirectional=False
|
bidirectional=False
|
||||||
)
|
)
|
||||||
relations = rel_extractor.extract_relations(text, entities=entities)
|
relations = rel_extractor.extract(text, entities=entities)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CoreferenceResolver
|
||||||
|
|
||||||
|
Resolves pronoun references and entity coreferences.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `method` | str or list | `None` | Underlying NER method(s) |
|
||||||
|
| `**config` | dict | `{}` | Configuration for NER method |
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `resolve(text)` | Alias for `resolve_coreferences`. Get coreference chains. |
|
||||||
|
| `resolve_coreferences(text)` | Get coreference chains |
|
||||||
|
| `resolve_pronouns(text)` | Resolve pronouns to entities |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from semantica.semantic_extract import CoreferenceResolver
|
||||||
|
|
||||||
|
resolver = CoreferenceResolver()
|
||||||
|
text = "Steve Jobs founded Apple. He was the CEO."
|
||||||
|
|
||||||
|
# Resolve references
|
||||||
|
chains = resolver.resolve(text)
|
||||||
|
# [CoreferenceChain(mentions=["Steve Jobs", "He"], representative="Steve Jobs")]
|
||||||
```
|
```
|
||||||
|
|
||||||
### EventDetector
|
### EventDetector
|
||||||
@@ -204,6 +286,7 @@ Extracts RDF triples (Subject-Predicate-Object).
|
|||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
| `include_temporal` | bool | `False` | Include time information |
|
| `include_temporal` | bool | `False` | Include time information |
|
||||||
| `include_provenance` | bool | `False` | Track source sentences |
|
| `include_provenance` | bool | `False` | Track source sentences |
|
||||||
|
| `method` | str | `"pattern"` | Extraction method ("pattern", "rules", "huggingface", "llm") |
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
@@ -224,6 +307,66 @@ triples = extractor.extract_triples("Steve Jobs founded Apple in 1976.")
|
|||||||
# [Triple(subject="Steve Jobs", predicate="founded", object="Apple", temporal="1976")]
|
# [Triple(subject="Steve Jobs", predicate="founded", object="Apple", temporal="1976")]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### SemanticNetworkExtractor
|
||||||
|
|
||||||
|
Extracts structured semantic networks with nodes and edges.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `ner_method` | str | `None` | Method for node extraction |
|
||||||
|
| `relation_method` | str | `None` | Method for edge extraction |
|
||||||
|
| `**config` | dict | `{}` | Configuration for underlying extractors |
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `extract_network(text)` | Build network from text |
|
||||||
|
| `extract(text)` | Alias for `extract_network` |
|
||||||
|
| `export_to_yaml(network, path)` | Save network to YAML |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from semantica.semantic_extract import SemanticNetworkExtractor
|
||||||
|
|
||||||
|
extractor = SemanticNetworkExtractor()
|
||||||
|
network = extractor.extract("Apple Inc. is located in Cupertino.")
|
||||||
|
|
||||||
|
# Analyze network
|
||||||
|
print(f"Nodes: {len(network.nodes)}")
|
||||||
|
print(f"Edges: {len(network.edges)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### LLMEnhancer
|
||||||
|
|
||||||
|
Enhances extraction results using Large Language Models.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `provider` | str | `"openai"` | LLM provider ("openai", "gemini", "anthropic", etc.) |
|
||||||
|
| `**config` | dict | `{}` | Model config (model name, api_key, etc.) |
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `enhance_entities(text, entities)` | Improve entity accuracy and details |
|
||||||
|
| `enhance_relations(text, relations)` | Improve relation detection |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from semantica.semantic_extract import LLMEnhancer
|
||||||
|
|
||||||
|
enhancer = LLMEnhancer(provider="openai", model="gpt-4")
|
||||||
|
enhanced_entities = enhancer.enhance_entities(text, entities)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
@@ -234,7 +377,8 @@ from semantica.semantic_extract import (
|
|||||||
RelationExtractor,
|
RelationExtractor,
|
||||||
TripleExtractor,
|
TripleExtractor,
|
||||||
EventDetector,
|
EventDetector,
|
||||||
CoreferenceResolver
|
CoreferenceResolver,
|
||||||
|
SemanticNetworkExtractor
|
||||||
)
|
)
|
||||||
|
|
||||||
text = "Apple released the iPhone in 2007. Steve Jobs announced it at Macworld."
|
text = "Apple released the iPhone in 2007. Steve Jobs announced it at Macworld."
|
||||||
@@ -259,10 +403,15 @@ triples = triple_extractor.extract_triples(text)
|
|||||||
event_detector = EventDetector(extract_time=True)
|
event_detector = EventDetector(extract_time=True)
|
||||||
events = event_detector.detect_events(text)
|
events = event_detector.detect_events(text)
|
||||||
|
|
||||||
|
# Extract semantic network
|
||||||
|
network_extractor = SemanticNetworkExtractor()
|
||||||
|
network = network_extractor.extract(text)
|
||||||
|
|
||||||
print(f"Entities: {len(entities)}")
|
print(f"Entities: {len(entities)}")
|
||||||
print(f"Relations: {len(relations)}")
|
print(f"Relations: {len(relations)}")
|
||||||
print(f"Triples: {len(triples)}")
|
print(f"Triples: {len(triples)}")
|
||||||
print(f"Events: {len(events)}")
|
print(f"Events: {len(events)}")
|
||||||
|
print(f"Network Nodes: {len(network.nodes)}")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+40
-60
@@ -150,7 +150,7 @@ TextSplitter(
|
|||||||
similarity_threshold=0.7, # Semantic boundary threshold
|
similarity_threshold=0.7, # Semantic boundary threshold
|
||||||
|
|
||||||
# Entity-aware options
|
# Entity-aware options
|
||||||
ner_method="spacy", # NER method (spacy, llm, transformers)
|
ner_method="ml", # NER method (ml/spacy, llm, pattern)
|
||||||
preserve_entities=True, # Don't split entities
|
preserve_entities=True, # Don't split entities
|
||||||
|
|
||||||
# LLM options
|
# LLM options
|
||||||
@@ -183,7 +183,7 @@ for i, chunk in enumerate(chunks):
|
|||||||
# Entity-aware for GraphRAG
|
# Entity-aware for GraphRAG
|
||||||
splitter = TextSplitter(
|
splitter = TextSplitter(
|
||||||
method="entity_aware",
|
method="entity_aware",
|
||||||
ner_method="llm",
|
ner_method="ml",
|
||||||
chunk_size=1000,
|
chunk_size=1000,
|
||||||
preserve_entities=True
|
preserve_entities=True
|
||||||
)
|
)
|
||||||
@@ -250,8 +250,6 @@ Preserve entity boundaries during chunking for GraphRAG.
|
|||||||
| Method | Description | Algorithm |
|
| Method | Description | Algorithm |
|
||||||
|--------|-------------|-----------|
|
|--------|-------------|-----------|
|
||||||
| `chunk(text, entities)` | Chunk preserving entities | Entity boundary detection |
|
| `chunk(text, entities)` | Chunk preserving entities | Entity boundary detection |
|
||||||
| `extract_entities(text)` | Extract entities | NER extraction |
|
|
||||||
| `find_safe_split_points(text, entities)` | Find split points | Entity span checking |
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -260,14 +258,14 @@ from semantica.split import EntityAwareChunker
|
|||||||
from semantica.semantic_extract import NERExtractor
|
from semantica.semantic_extract import NERExtractor
|
||||||
|
|
||||||
# Extract entities first
|
# Extract entities first
|
||||||
ner = NERExtractor(method="llm")
|
ner = NERExtractor(method="ml")
|
||||||
entities = ner.extract(text)
|
entities = ner.extract(text)
|
||||||
|
|
||||||
# Chunk preserving entities
|
# Chunk preserving entities
|
||||||
chunker = EntityAwareChunker(
|
chunker = EntityAwareChunker(
|
||||||
chunk_size=1000,
|
chunk_size=1000,
|
||||||
chunk_overlap=200,
|
chunk_overlap=200,
|
||||||
ner_method="llm"
|
ner_method="ml"
|
||||||
)
|
)
|
||||||
|
|
||||||
chunks = chunker.chunk(text, entities=entities)
|
chunks = chunker.chunk(text, entities=entities)
|
||||||
@@ -360,8 +358,7 @@ Structure-aware chunking respecting document hierarchy.
|
|||||||
| Method | Description | Algorithm |
|
| Method | Description | Algorithm |
|
||||||
|--------|-------------|-----------|
|
|--------|-------------|-----------|
|
||||||
| `chunk(text)` | Chunk by structure | Heading/section detection |
|
| `chunk(text)` | Chunk by structure | Heading/section detection |
|
||||||
| `detect_structure(text)` | Detect document structure | Markdown/HTML parsing |
|
| `_extract_structure(text)` | Extract structural elements | Markdown/HTML parsing |
|
||||||
| `build_hierarchy(sections)` | Build section hierarchy | Tree construction |
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -369,17 +366,16 @@ Structure-aware chunking respecting document hierarchy.
|
|||||||
from semantica.split import StructuralChunker
|
from semantica.split import StructuralChunker
|
||||||
|
|
||||||
chunker = StructuralChunker(
|
chunker = StructuralChunker(
|
||||||
respect_headings=True,
|
respect_headers=True,
|
||||||
respect_paragraphs=True,
|
respect_sections=True,
|
||||||
respect_lists=True,
|
|
||||||
max_chunk_size=2000
|
max_chunk_size=2000
|
||||||
)
|
)
|
||||||
|
|
||||||
chunks = chunker.chunk(markdown_text)
|
chunks = chunker.chunk(markdown_text)
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
print(f"Section: {chunk.metadata.get('section_title')}")
|
print(f"Structure preserved: {chunk.metadata.get('structure_preserved')}")
|
||||||
print(f"Level: {chunk.metadata.get('heading_level')}")
|
print(f"Elements: {chunk.metadata.get('element_types')}")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -393,7 +389,6 @@ Multi-level hierarchical chunking.
|
|||||||
| Method | Description | Algorithm |
|
| Method | Description | Algorithm |
|
||||||
|--------|-------------|-----------|
|
|--------|-------------|-----------|
|
||||||
| `chunk(text)` | Multi-level chunking | Recursive hierarchical split |
|
| `chunk(text)` | Multi-level chunking | Recursive hierarchical split |
|
||||||
| `create_hierarchy(chunks)` | Create chunk hierarchy | Tree structure |
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -470,16 +465,15 @@ Fixed-size sliding window chunking with configurable step size.
|
|||||||
| Method | Description | Algorithm |
|
| Method | Description | Algorithm |
|
||||||
|--------|-------------|-----------|
|
|--------|-------------|-----------|
|
||||||
| `chunk(text)` | Sliding window chunking | Fixed-size window with step |
|
| `chunk(text)` | Sliding window chunking | Fixed-size window with step |
|
||||||
| `calculate_windows(text_length)` | Calculate window positions | Window position calculation |
|
| `chunk_with_overlap(text)` | Chunk with specific overlap | Window position calculation |
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
| `window_size` | int | 1000 | Size of sliding window |
|
| `chunk_size` | int | 1000 | Size of sliding window |
|
||||||
| `step_size` | int | 800 | Step size (window_size - overlap) |
|
| `overlap` | int | 0 | Overlap size |
|
||||||
| `min_chunk_size` | int | 100 | Minimum chunk size |
|
| `stride` | int | chunk_size - overlap | Step size |
|
||||||
| `preserve_sentences` | bool | False | Preserve sentence boundaries |
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -488,25 +482,18 @@ from semantica.split import SlidingWindowChunker
|
|||||||
|
|
||||||
# Basic sliding window
|
# Basic sliding window
|
||||||
chunker = SlidingWindowChunker(
|
chunker = SlidingWindowChunker(
|
||||||
window_size=1000,
|
chunk_size=1000,
|
||||||
step_size=800, # 200 overlap
|
overlap=200
|
||||||
min_chunk_size=100
|
|
||||||
)
|
)
|
||||||
|
|
||||||
chunks = chunker.chunk(long_text)
|
chunks = chunker.chunk(long_text)
|
||||||
|
|
||||||
for i, chunk in enumerate(chunks):
|
for i, chunk in enumerate(chunks):
|
||||||
print(f"Window {i}: chars {chunk.start}-{chunk.end}")
|
print(f"Window {i}: chars {chunk.start_index}-{chunk.end_index}")
|
||||||
print(f"Overlap with previous: {chunk.metadata.get('overlap_chars')}")
|
print(f"Has overlap: {chunk.metadata.get('has_overlap')}")
|
||||||
|
|
||||||
# Sentence-preserving sliding window
|
# Boundary-preserving sliding window
|
||||||
chunker = SlidingWindowChunker(
|
chunks = chunker.chunk(text, preserve_boundaries=True)
|
||||||
window_size=1000,
|
|
||||||
step_size=750,
|
|
||||||
preserve_sentences=True
|
|
||||||
)
|
|
||||||
|
|
||||||
chunks = chunker.chunk(text)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -519,18 +506,17 @@ Table-specific chunking preserving table structure.
|
|||||||
|
|
||||||
| Method | Description | Algorithm |
|
| Method | Description | Algorithm |
|
||||||
|--------|-------------|-----------|
|
|--------|-------------|-----------|
|
||||||
| `chunk(text)` | Chunk tables | Table detection and splitting |
|
| `chunk_table(table_data)` | Chunk tables | Row/Column-based splitting |
|
||||||
| `detect_tables(text)` | Detect tables in text | Table boundary detection |
|
| `chunk_to_text_chunks(table_data)` | Convert table chunks to text | Table to text conversion |
|
||||||
| `split_table(table, max_rows)` | Split large tables | Row-based table splitting |
|
| `extract_table_schema(table_data)` | Extract schema | Type inference and schema extraction |
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
|
| `max_rows` | int | 100 | Maximum rows per table chunk |
|
||||||
| `preserve_headers` | bool | True | Keep headers in each chunk |
|
| `preserve_headers` | bool | True | Keep headers in each chunk |
|
||||||
| `max_rows_per_chunk` | int | 50 | Maximum rows per table chunk |
|
| `chunk_by_columns` | bool | False | Chunk by columns instead of rows |
|
||||||
| `include_context` | bool | True | Include surrounding text context |
|
|
||||||
| `table_format` | str | "auto" | Table format (markdown, html, csv, auto) |
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -538,31 +524,25 @@ Table-specific chunking preserving table structure.
|
|||||||
from semantica.split import TableChunker
|
from semantica.split import TableChunker
|
||||||
|
|
||||||
chunker = TableChunker(
|
chunker = TableChunker(
|
||||||
|
max_rows=50,
|
||||||
preserve_headers=True,
|
preserve_headers=True,
|
||||||
max_rows_per_chunk=50,
|
chunk_by_columns=False
|
||||||
include_context=True,
|
|
||||||
table_format="markdown"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
text_with_tables = \"\"\"
|
table_data = {
|
||||||
Document with tables...
|
"headers": ["Col1", "Col2", "Col3"],
|
||||||
|
"rows": [["Val1", "Val2", "Val3"], ...]
|
||||||
|
}
|
||||||
|
|
||||||
| Column 1 | Column 2 | Column 3 |
|
# Get structured table chunks
|
||||||
|----------|----------|----------|
|
table_chunks = chunker.chunk_table(table_data)
|
||||||
| Value 1 | Value 2 | Value 3 |
|
|
||||||
| ... | ... | ... |
|
|
||||||
\"\"\"
|
|
||||||
|
|
||||||
chunks = chunker.chunk(text_with_tables)
|
# Get text chunks for RAG
|
||||||
|
text_chunks = chunker.chunk_to_text_chunks(table_data)
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in text_chunks:
|
||||||
if chunk.metadata.get('is_table'):
|
print(f"Table chunk {chunk.metadata.get('chunk_index')}")
|
||||||
print(f"Table chunk:")
|
print(f"Rows: {chunk.metadata.get('row_count')}")
|
||||||
print(f" Rows: {chunk.metadata.get('row_count')}")
|
|
||||||
print(f" Columns: {chunk.metadata.get('column_count')}")
|
|
||||||
print(f" Headers: {chunk.metadata.get('headers')}")
|
|
||||||
else:
|
|
||||||
print(f"Text chunk: {len(chunk.text)} chars")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -663,7 +643,7 @@ print(f"Available methods: {methods}")
|
|||||||
# Quick splitting
|
# Quick splitting
|
||||||
chunks = split_recursive(text, chunk_size=1000, chunk_overlap=200)
|
chunks = split_recursive(text, chunk_size=1000, chunk_overlap=200)
|
||||||
chunks = split_by_sentences(text, sentences_per_chunk=5)
|
chunks = split_by_sentences(text, sentences_per_chunk=5)
|
||||||
chunks = split_entity_aware(text, ner_method="llm")
|
chunks = split_entity_aware(text, ner_method="ml")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -683,7 +663,7 @@ export SPLIT_EMBEDDING_MODEL=all-MiniLM-L6-v2
|
|||||||
export SPLIT_SIMILARITY_THRESHOLD=0.7
|
export SPLIT_SIMILARITY_THRESHOLD=0.7
|
||||||
|
|
||||||
# Entity-aware
|
# Entity-aware
|
||||||
export SPLIT_NER_METHOD=spacy
|
export SPLIT_NER_METHOD=ml # or spacy
|
||||||
export SPLIT_PRESERVE_ENTITIES=true
|
export SPLIT_PRESERVE_ENTITIES=true
|
||||||
|
|
||||||
# LLM-based
|
# LLM-based
|
||||||
@@ -712,7 +692,7 @@ split:
|
|||||||
max_chunk_size: 2000
|
max_chunk_size: 2000
|
||||||
|
|
||||||
entity_aware:
|
entity_aware:
|
||||||
ner_method: spacy
|
ner_method: ml # or spacy
|
||||||
preserve_entities: true
|
preserve_entities: true
|
||||||
min_entity_gap: 50
|
min_entity_gap: 50
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
test content
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def log(msg):
|
|
||||||
with open(r"C:\Users\Mohd Kaif\semantica\pr_log.txt", "a") as f:
|
|
||||||
f.write(msg + "\n")
|
|
||||||
print(msg)
|
|
||||||
|
|
||||||
def run_command(command):
|
|
||||||
log(f"Running: {command}")
|
|
||||||
try:
|
|
||||||
result = subprocess.run(command, shell=True, check=False, capture_output=True, text=True)
|
|
||||||
log("STDOUT: " + result.stdout)
|
|
||||||
log("STDERR: " + result.stderr)
|
|
||||||
return result.stdout
|
|
||||||
except Exception as e:
|
|
||||||
log(f"Exception: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
with open(r"C:\Users\Mohd Kaif\semantica\pr_log.txt", "w") as f:
|
|
||||||
f.write("Starting PR process\n")
|
|
||||||
|
|
||||||
log("--- Pushing to origin ---")
|
|
||||||
run_command("git push origin knowledge-engineering")
|
|
||||||
|
|
||||||
log("\n--- Checking PR list ---")
|
|
||||||
pr_list = run_command("gh pr list --head knowledge-engineering")
|
|
||||||
|
|
||||||
if pr_list is not None and "knowledge-engineering" not in pr_list:
|
|
||||||
log("\n--- Creating PR ---")
|
|
||||||
run_command('gh pr create --title "feat: Knowledge Engineering Module Enhancements and Testing" --body "Enhancements to KG module including unit tests, conflict resolution placeholders, and documentation updates." --head knowledge-engineering --base main')
|
|
||||||
else:
|
|
||||||
log("\n--- PR might already exist ---")
|
|
||||||
log(f"PR List output: {pr_list}")
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
def run_tests():
|
|
||||||
print("SCRIPT STARTED")
|
|
||||||
log_path = os.path.join(os.getcwd(), "normalize_results_v3.log")
|
|
||||||
print(f"Writing log to {log_path}")
|
|
||||||
|
|
||||||
# Ensure we can import from semantica
|
|
||||||
sys.path.append(os.getcwd())
|
|
||||||
|
|
||||||
try:
|
|
||||||
loader = unittest.TestLoader()
|
|
||||||
start_dir = 'tests/normalize'
|
|
||||||
print(f"Discovering tests in {start_dir}")
|
|
||||||
suite = loader.discover(start_dir)
|
|
||||||
|
|
||||||
print(f"Discovered {suite.countTestCases()} tests.")
|
|
||||||
|
|
||||||
runner = unittest.TextTestRunner(verbosity=2)
|
|
||||||
|
|
||||||
# Open a log file to write results
|
|
||||||
with open(log_path, 'w') as f:
|
|
||||||
f.write("Test Execution Log:\n")
|
|
||||||
f.write("===================\n\n")
|
|
||||||
|
|
||||||
# Use a custom runner that prints to both stdout and the file
|
|
||||||
class TeeStream:
|
|
||||||
def __init__(self, stream1, stream2):
|
|
||||||
self.stream1 = stream1
|
|
||||||
self.stream2 = stream2
|
|
||||||
def write(self, data):
|
|
||||||
self.stream1.write(data)
|
|
||||||
self.stream2.write(data)
|
|
||||||
def flush(self):
|
|
||||||
self.stream1.flush()
|
|
||||||
self.stream2.flush()
|
|
||||||
|
|
||||||
runner = unittest.TextTestRunner(stream=TeeStream(sys.stdout, f), verbosity=2)
|
|
||||||
result = runner.run(suite)
|
|
||||||
|
|
||||||
if result.wasSuccessful():
|
|
||||||
print("ALL TESTS PASSED")
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print("SOME TESTS FAILED")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An error occurred: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_tests()
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[logging.StreamHandler(sys.stdout)]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger("verify_conflict_complete")
|
|
||||||
|
|
||||||
def verify_conflict_complete():
|
|
||||||
print("=== Starting Complete Conflict Module Verification ===\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from semantica.conflicts import (
|
|
||||||
ConflictDetector, ConflictResolver, ConflictAnalyzer,
|
|
||||||
SourceTracker, InvestigationGuideGenerator, SourceReference,
|
|
||||||
method_registry, ResolutionResult, ConflictsConfig, conflicts_config
|
|
||||||
)
|
|
||||||
from semantica.conflicts.methods import (
|
|
||||||
detect_conflicts, resolve_conflicts, analyze_conflicts,
|
|
||||||
track_sources, generate_investigation_guide,
|
|
||||||
list_available_methods, get_conflict_method
|
|
||||||
)
|
|
||||||
print("[1] Imports successful.\n")
|
|
||||||
except ImportError as e:
|
|
||||||
print(f"Error importing semantica.conflicts: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# --- Step 1: Conflict Detection (Comprehensive) ---
|
|
||||||
print("[2] Testing Comprehensive Conflict Detection...")
|
|
||||||
detector = ConflictDetector(
|
|
||||||
confidence_threshold=0.7,
|
|
||||||
track_provenance=True,
|
|
||||||
conflict_fields={"Company": ["name", "founded", "revenue"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
entities = [
|
|
||||||
{"id": "e1", "name": "Apple Inc.", "founded": 1976, "type": "Company",
|
|
||||||
"source": "wikipedia", "confidence": 0.9, "metadata": {"timestamp": datetime(2023, 1, 15)}},
|
|
||||||
{"id": "e1", "name": "Apple Incorporated", "founded": 1976, "type": "Company",
|
|
||||||
"source": "official_site", "confidence": 0.95, "metadata": {"timestamp": datetime(2023, 3, 20)}},
|
|
||||||
{"id": "e1", "name": "Apple Inc.", "founded": 1977, "type": "Company",
|
|
||||||
"source": "news", "confidence": 0.7, "metadata": {"timestamp": datetime(2023, 2, 10)}},
|
|
||||||
{"id": "e2", "name": "Microsoft", "type": "Company", "founded": 1975, "source": "source1"},
|
|
||||||
{"id": "e2", "name": "Microsoft Corporation", "type": "Organization",
|
|
||||||
"founded": 1975, "source": "source2"},
|
|
||||||
]
|
|
||||||
|
|
||||||
# 1.1 Value Conflicts
|
|
||||||
value_conflicts = detector.detect_value_conflicts(entities, "name")
|
|
||||||
print(f" Value Conflicts Detected: {len(value_conflicts)}")
|
|
||||||
|
|
||||||
# 1.2 Type Conflicts
|
|
||||||
type_conflicts = detector.detect_type_conflicts(entities)
|
|
||||||
print(f" Type Conflicts Detected: {len(type_conflicts)}")
|
|
||||||
|
|
||||||
# 1.3 Temporal Conflicts (founded date mismatch)
|
|
||||||
# Note: detect_temporal_conflicts usually checks for logical temporal issues or changing values over time
|
|
||||||
temporal_conflicts = detector.detect_temporal_conflicts(entities)
|
|
||||||
print(f" Temporal Conflicts Detected: {len(temporal_conflicts)}")
|
|
||||||
|
|
||||||
# 1.6 General Detection
|
|
||||||
all_conflicts = detector.detect_conflicts(entities)
|
|
||||||
print(f" Total Conflicts Detected (General): {len(all_conflicts)}")
|
|
||||||
|
|
||||||
# --- Step 2: Source Tracking ---
|
|
||||||
print("\n[3] Testing Source Tracking...")
|
|
||||||
tracker = SourceTracker()
|
|
||||||
source1 = SourceReference(document="wikipedia", timestamp=datetime(2023, 1, 15), confidence=0.9)
|
|
||||||
source2 = SourceReference(document="official_site", timestamp=datetime(2023, 3, 20), confidence=0.95)
|
|
||||||
|
|
||||||
tracker.track_property_source("e1", "name", "Apple Inc.", source1)
|
|
||||||
tracker.track_property_source("e1", "name", "Apple Incorporated", source2)
|
|
||||||
tracker.set_source_credibility("wikipedia", 0.85)
|
|
||||||
tracker.set_source_credibility("official_site", 0.95)
|
|
||||||
|
|
||||||
sources = tracker.get_property_sources("e1", "name")
|
|
||||||
print(f" Sources for e1.name: {len(sources.sources) if sources else 0}")
|
|
||||||
|
|
||||||
# --- Step 3: Conflict Resolution (Advanced) ---
|
|
||||||
print("\n[4] Testing Advanced Conflict Resolution...")
|
|
||||||
resolver = ConflictResolver(default_strategy="voting", source_tracker=tracker)
|
|
||||||
|
|
||||||
if value_conflicts:
|
|
||||||
# Credibility Weighted
|
|
||||||
results = resolver.resolve_conflicts(value_conflicts, strategy="credibility_weighted")
|
|
||||||
for r in results:
|
|
||||||
if r.resolved:
|
|
||||||
print(f" Resolved (Credibility): {r.resolved_value} (Conf: {r.confidence:.2f})")
|
|
||||||
|
|
||||||
# --- Step 4: Conflict Analysis ---
|
|
||||||
print("\n[5] Testing Conflict Analyzer...")
|
|
||||||
analyzer = ConflictAnalyzer()
|
|
||||||
analysis = analyzer.analyze_conflicts(all_conflicts)
|
|
||||||
print(f" Analysis Keys: {list(analysis.keys())}")
|
|
||||||
print(f" Total Conflicts in Analysis: {analysis.get('total_conflicts', 0)}")
|
|
||||||
|
|
||||||
# Check insights report
|
|
||||||
try:
|
|
||||||
insights = analyzer.generate_insights_report(all_conflicts)
|
|
||||||
print(f" Insights Report Generated (Length: {len(insights)})")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error generating insights report: {e}")
|
|
||||||
|
|
||||||
# --- Step 5: Investigation Guides ---
|
|
||||||
print("\n[6] Testing Investigation Guide Generator...")
|
|
||||||
guide_generator = InvestigationGuideGenerator(source_tracker=tracker)
|
|
||||||
|
|
||||||
if value_conflicts:
|
|
||||||
guide = guide_generator.generate_guide(value_conflicts[0])
|
|
||||||
print(f" Guide Generated for: {guide.conflict_id}")
|
|
||||||
print(f" Recommended Actions: {guide.recommended_actions}")
|
|
||||||
|
|
||||||
checklist = guide_generator.export_investigation_checklist(guide, format="markdown")
|
|
||||||
print(f" Checklist Exported (Markdown length: {len(checklist)})")
|
|
||||||
|
|
||||||
# --- Step 6: Methods Module (Functional API) ---
|
|
||||||
print("\n[7] Testing Functional API...")
|
|
||||||
try:
|
|
||||||
# Detection
|
|
||||||
func_conflicts = detect_conflicts(entities, method="value", property_name="name")
|
|
||||||
print(f" Functional Detect (Value): {len(func_conflicts)}")
|
|
||||||
|
|
||||||
# Resolution
|
|
||||||
if func_conflicts:
|
|
||||||
func_results = resolve_conflicts(func_conflicts, method="voting")
|
|
||||||
print(f" Functional Resolve (Voting): {len(func_results)}")
|
|
||||||
|
|
||||||
# Analysis
|
|
||||||
func_analysis = analyze_conflicts(all_conflicts, method="pattern")
|
|
||||||
print(f" Functional Analyze (Pattern): {len(func_analysis) if func_analysis else 0} patterns found")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error in Functional API: {e}")
|
|
||||||
|
|
||||||
# --- Step 7: Method Registry ---
|
|
||||||
print("\n[8] Testing Method Registry...")
|
|
||||||
def custom_resolution_func(conflicts, **kwargs):
|
|
||||||
results = []
|
|
||||||
for conflict in conflicts:
|
|
||||||
res = ResolutionResult(
|
|
||||||
conflict_id=conflict.conflict_id,
|
|
||||||
resolved=True,
|
|
||||||
resolved_value="CUSTOM_VALUE",
|
|
||||||
resolution_strategy="custom_test"
|
|
||||||
)
|
|
||||||
results.append(res)
|
|
||||||
return results
|
|
||||||
|
|
||||||
method_registry.register("resolution", "custom_test", custom_resolution_func)
|
|
||||||
print(" Registered 'custom_test' method.")
|
|
||||||
|
|
||||||
reg_methods = method_registry.list_all("resolution")
|
|
||||||
if "custom_test" in reg_methods.get("resolution", []):
|
|
||||||
print(" Verified 'custom_test' in registry.")
|
|
||||||
|
|
||||||
# Test usage
|
|
||||||
if value_conflicts:
|
|
||||||
custom_res = resolve_conflicts(value_conflicts, method="custom_test")
|
|
||||||
print(f" Custom Resolution Result: {custom_res[0].resolved_value}")
|
|
||||||
|
|
||||||
method_registry.unregister("resolution", "custom_test")
|
|
||||||
print(" Unregistered 'custom_test'.")
|
|
||||||
|
|
||||||
# --- Step 8: Configuration ---
|
|
||||||
print("\n[9] Testing Configuration...")
|
|
||||||
conflicts_config.set("confidence_threshold", 0.85)
|
|
||||||
val = conflicts_config.get("confidence_threshold")
|
|
||||||
print(f" Config Value Set/Get: {val}")
|
|
||||||
|
|
||||||
print("\n=== Complete Conflict Module Verification Finished ===")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
verify_conflict_complete()
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[logging.StreamHandler(sys.stdout)]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger("verify_conflict_resolution")
|
|
||||||
|
|
||||||
def verify_conflict_resolution():
|
|
||||||
print("=== Starting Conflict Resolution Verification ===\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from semantica.conflicts import ConflictDetector, ConflictResolver, SourceTracker
|
|
||||||
from semantica.conflicts.conflict_resolver import ResolutionStrategy
|
|
||||||
print("[1] Imports successful.\n")
|
|
||||||
except ImportError as e:
|
|
||||||
print(f"Error importing semantica.conflicts: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Step 1: Define Entities with Conflicting Data
|
|
||||||
print("[2] Defining conflicting entities...")
|
|
||||||
entities = [
|
|
||||||
{
|
|
||||||
"id": "e1",
|
|
||||||
"type": "Person",
|
|
||||||
"name": "John Doe",
|
|
||||||
"age": 30,
|
|
||||||
"location": "New York",
|
|
||||||
"source": "source1",
|
|
||||||
"confidence": 0.8,
|
|
||||||
"metadata": {"timestamp": datetime(2023, 1, 1)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e1",
|
|
||||||
"type": "Person",
|
|
||||||
"name": "John Doe",
|
|
||||||
"age": 32,
|
|
||||||
"location": "Boston",
|
|
||||||
"source": "source2",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"metadata": {"timestamp": datetime(2023, 6, 1)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e2",
|
|
||||||
"type": "Organization",
|
|
||||||
"name": "Tech Corp",
|
|
||||||
"founded": 2010,
|
|
||||||
"employees": 100,
|
|
||||||
"source": "source1",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"metadata": {"timestamp": datetime(2023, 1, 1)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e2",
|
|
||||||
"type": "Organization",
|
|
||||||
"name": "Tech Corp",
|
|
||||||
"founded": 2012,
|
|
||||||
"employees": 150,
|
|
||||||
"source": "source2",
|
|
||||||
"confidence": 0.7,
|
|
||||||
"metadata": {"timestamp": datetime(2023, 3, 1)}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
print(f" defined {len(entities)} entity records.\n")
|
|
||||||
|
|
||||||
# Step 2: Detect Conflicts
|
|
||||||
print("[3] Detecting conflicts...")
|
|
||||||
detector = ConflictDetector(track_provenance=True)
|
|
||||||
conflicts = detector.detect_entity_conflicts(entities)
|
|
||||||
|
|
||||||
print(f"Detected {len(conflicts)} conflicts.")
|
|
||||||
for i, conflict in enumerate(conflicts, 1):
|
|
||||||
print(f" Conflict {i}: Entity={conflict.entity_id}, Property={conflict.property_name}, Values={conflict.conflicting_values}")
|
|
||||||
|
|
||||||
if len(conflicts) == 0:
|
|
||||||
print("WARNING: No conflicts detected! Verification cannot proceed meaningfully.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Step 3: Resolve Conflicts
|
|
||||||
resolver = ConflictResolver()
|
|
||||||
|
|
||||||
# Strategy: Voting
|
|
||||||
print("\n[4a] Resolving with 'voting' strategy...")
|
|
||||||
results_voting = resolver.resolve_conflicts(conflicts, strategy="voting")
|
|
||||||
for r in results_voting:
|
|
||||||
if r.resolved:
|
|
||||||
print(f" Resolved {r.conflict_id} ({r.resolution_strategy}): {r.resolved_value}")
|
|
||||||
|
|
||||||
# Strategy: Most Recent
|
|
||||||
print("\n[4b] Resolving with 'most_recent' strategy...")
|
|
||||||
results_recent = resolver.resolve_conflicts(conflicts, strategy="most_recent")
|
|
||||||
for r in results_recent:
|
|
||||||
if r.resolved:
|
|
||||||
print(f" Resolved {r.conflict_id} ({r.resolution_strategy}): {r.resolved_value}")
|
|
||||||
# Verification for e1 (most recent is source2 -> age 32, location Boston)
|
|
||||||
# Verification for e2 (most recent is source2 -> founded 2012, employees 150)
|
|
||||||
|
|
||||||
# Note: We can't easily map back to entity ID without parsing conflict_id or checking logic,
|
|
||||||
# but we can verify the values exist in our expectations.
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Strategy: Highest Confidence
|
|
||||||
print("\n[4c] Resolving with 'highest_confidence' strategy...")
|
|
||||||
results_confidence = resolver.resolve_conflicts(conflicts, strategy="highest_confidence")
|
|
||||||
for r in results_confidence:
|
|
||||||
if r.resolved:
|
|
||||||
print(f" Resolved {r.conflict_id} ({r.resolution_strategy}): {r.resolved_value}")
|
|
||||||
# Verification for e1 (highest conf is source2 -> age 32)
|
|
||||||
# Verification for e2 (highest conf is source1 -> founded 2010)
|
|
||||||
|
|
||||||
# Step 4: Track Sources
|
|
||||||
print("\n[5] Tracking sources...")
|
|
||||||
tracker = detector.source_tracker
|
|
||||||
for conflict in conflicts:
|
|
||||||
sources = tracker.get_property_sources(conflict.entity_id, conflict.property_name)
|
|
||||||
if sources:
|
|
||||||
print(f" Entity: {conflict.entity_id}, Property: {conflict.property_name}, Source Count: {len(sources.sources)}")
|
|
||||||
|
|
||||||
# Step 5: Audit Trail
|
|
||||||
print("\n[6] Checking audit trail...")
|
|
||||||
history = resolver.get_resolution_history()
|
|
||||||
print(f" History entries: {len(history)}")
|
|
||||||
if len(history) > 0:
|
|
||||||
last_entry = history[-1]
|
|
||||||
print(f" Last entry: Strategy={last_entry.resolution_strategy}, Value={last_entry.resolved_value}")
|
|
||||||
|
|
||||||
print("\n=== Conflict Resolution Verification Completed Successfully ===")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
verify_conflict_resolution()
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
"""
|
|
||||||
Script to verify the usage of the Semantica Core Module.
|
|
||||||
This simulates the typical usage pattern described in core_usage.md.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Add project root to path to ensure we can import semantica
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
||||||
|
|
||||||
from semantica import Semantica
|
|
||||||
from semantica.core import LifecycleManager, PluginRegistry
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
||||||
logger = logging.getLogger("verify_core")
|
|
||||||
|
|
||||||
def custom_startup_hook():
|
|
||||||
logger.info("✅ Custom startup hook executed!")
|
|
||||||
|
|
||||||
def custom_processing_method(sources, **kwargs):
|
|
||||||
logger.info(f"✅ Custom processing method executed for sources: {sources}")
|
|
||||||
return {"status": "success", "processed_items": len(sources)}
|
|
||||||
|
|
||||||
def main():
|
|
||||||
logger.info("Starting Core Module Verification...")
|
|
||||||
|
|
||||||
# 1. Initialize Semantica
|
|
||||||
logger.info("\n--- Step 1: Initialization ---")
|
|
||||||
config = {
|
|
||||||
"project_name": "CoreVerification",
|
|
||||||
"logging": {"level": "DEBUG"}
|
|
||||||
}
|
|
||||||
app = Semantica(config)
|
|
||||||
logger.info("Semantica instance created.")
|
|
||||||
|
|
||||||
# 2. Register Hooks via Lifecycle Manager
|
|
||||||
logger.info("\n--- Step 2: Lifecycle Hooks ---")
|
|
||||||
app.lifecycle_manager.register_startup_hook(custom_startup_hook, priority=10)
|
|
||||||
logger.info("Startup hook registered.")
|
|
||||||
|
|
||||||
# 3. Register Custom Method
|
|
||||||
logger.info("\n--- Step 3: Method Registry ---")
|
|
||||||
from semantica.core.registry import method_registry
|
|
||||||
method_registry.register("knowledge_base", "custom_processor", custom_processing_method)
|
|
||||||
logger.info("Custom method 'custom_processor' registered.")
|
|
||||||
|
|
||||||
# 4. Start the System (Initialize)
|
|
||||||
logger.info("\n--- Step 4: System Startup ---")
|
|
||||||
app.initialize()
|
|
||||||
|
|
||||||
# Check health
|
|
||||||
health = app.lifecycle_manager.get_health_summary()
|
|
||||||
logger.info(f"System Health: {'Healthy' if health['is_healthy'] else 'Unhealthy'}")
|
|
||||||
if not health['is_healthy']:
|
|
||||||
logger.warning(f"Unhealthy components: {health['unhealthy_components']}")
|
|
||||||
|
|
||||||
# 5. Run a Workflow using the Custom Method
|
|
||||||
logger.info("\n--- Step 5: Workflow Execution ---")
|
|
||||||
sources = ["file1.txt", "file2.txt"]
|
|
||||||
# We use the 'method' argument which the orchestrator (via methods.py) uses to look up the registry
|
|
||||||
# Note: orchestrator.build_knowledge_base doesn't directly expose 'method' arg in signature but passes **kwargs to implementation
|
|
||||||
# Let's check how methods.py is called.
|
|
||||||
# build_knowledge_base calls build_knowledge_base (wrapper) in methods.py?
|
|
||||||
# Wait, orchestrator.py: build_knowledge_base calls self._create_pipeline...
|
|
||||||
|
|
||||||
# Actually, looking at orchestrator.py:
|
|
||||||
# It calls self._create_pipeline(pipeline_config)
|
|
||||||
# It doesn't seem to directly use 'method_registry' for the main 'build_knowledge_base' flow in the default implementation.
|
|
||||||
# However, methods.py defines 'build_knowledge_base' which IS the implementation used if imported as functional API.
|
|
||||||
# But Semantica class in orchestrator.py has its own build_knowledge_base method.
|
|
||||||
|
|
||||||
# Let's see if we can use the method registry via the functional API or if we need to check how Semantica class uses it.
|
|
||||||
# The Semantica class seems to have a hardcoded implementation in build_knowledge_base that creates a pipeline.
|
|
||||||
# But wait, semantica/__init__.py likely exposes the class.
|
|
||||||
|
|
||||||
# Let's try to invoke the custom method directly to verify registry,
|
|
||||||
# OR if Semantica class supports delegation (it might not currently).
|
|
||||||
|
|
||||||
# Let's verify the functional API wrapper usage as well.
|
|
||||||
from semantica.core.methods import build_knowledge_base as functional_build_kb
|
|
||||||
|
|
||||||
result = functional_build_kb(sources, method="custom_processor", config=config)
|
|
||||||
logger.info(f"Functional API Result: {result}")
|
|
||||||
|
|
||||||
# 6. Shutdown
|
|
||||||
logger.info("\n--- Step 6: Shutdown ---")
|
|
||||||
app.lifecycle_manager.shutdown()
|
|
||||||
logger.info("System shutdown completed.")
|
|
||||||
|
|
||||||
logger.info("\n✅ Verification Completed Successfully!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def verify_deduplication_notebook():
|
|
||||||
print("=== Starting Deduplication Notebook Verification ===")
|
|
||||||
|
|
||||||
# Import all deduplication classes
|
|
||||||
print("\n[1] Importing deduplication classes...")
|
|
||||||
from semantica.deduplication import (
|
|
||||||
# Main Classes
|
|
||||||
DuplicateDetector,
|
|
||||||
EntityMerger,
|
|
||||||
SimilarityCalculator,
|
|
||||||
ClusterBuilder,
|
|
||||||
MergeStrategyManager,
|
|
||||||
MethodRegistry,
|
|
||||||
DeduplicationConfig,
|
|
||||||
# Data Classes
|
|
||||||
DuplicateCandidate,
|
|
||||||
DuplicateGroup,
|
|
||||||
MergeOperation,
|
|
||||||
SimilarityResult,
|
|
||||||
Cluster,
|
|
||||||
ClusterResult,
|
|
||||||
MergeResult,
|
|
||||||
MergeStrategy,
|
|
||||||
# Global Instances
|
|
||||||
method_registry,
|
|
||||||
dedup_config,
|
|
||||||
)
|
|
||||||
print("Imports successful.")
|
|
||||||
|
|
||||||
# Create sample entities with potential duplicates
|
|
||||||
print("\n[2] Creating sample entities...")
|
|
||||||
entities = [
|
|
||||||
{
|
|
||||||
"id": "e1",
|
|
||||||
"name": "Apple Inc.",
|
|
||||||
"type": "Company",
|
|
||||||
"founded": 1976,
|
|
||||||
"properties": {"industry": "Technology", "headquarters": "Cupertino"},
|
|
||||||
"relationships": [{"subject": "e1", "predicate": "founded_by", "object": "Steve Jobs"}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e2",
|
|
||||||
"name": "Apple Inc",
|
|
||||||
"type": "Company",
|
|
||||||
"founded": 1976,
|
|
||||||
"properties": {"industry": "Tech", "headquarters": "Cupertino, CA"},
|
|
||||||
"relationships": [{"subject": "e2", "predicate": "founded_by", "object": "Steve Jobs"}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e3",
|
|
||||||
"name": "Microsoft Corporation",
|
|
||||||
"type": "Company",
|
|
||||||
"founded": 1975,
|
|
||||||
"properties": {"industry": "Technology", "headquarters": "Redmond"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e4",
|
|
||||||
"name": "Microsoft",
|
|
||||||
"type": "Company",
|
|
||||||
"founded": 1975,
|
|
||||||
"properties": {"industry": "Tech", "headquarters": "Redmond, WA"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e5",
|
|
||||||
"name": "Google LLC",
|
|
||||||
"type": "Company",
|
|
||||||
"founded": 1998,
|
|
||||||
"properties": {"industry": "Technology"},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
print(f"Created {len(entities)} sample entities")
|
|
||||||
print("Entity names:")
|
|
||||||
for e in entities:
|
|
||||||
print(f" - {e['name']} (ID: {e['id']})")
|
|
||||||
|
|
||||||
# Example: Duplicate Detection
|
|
||||||
print("\n[3] Testing Duplicate Detection...")
|
|
||||||
|
|
||||||
# Initialize DuplicateDetector
|
|
||||||
detector = DuplicateDetector(
|
|
||||||
similarity_threshold=0.7,
|
|
||||||
confidence_threshold=0.6,
|
|
||||||
# use_clustering=True, # Note: Constructor signature might verify this
|
|
||||||
)
|
|
||||||
|
|
||||||
# Detect duplicate candidates (pairwise)
|
|
||||||
print(" Running pairwise detection...")
|
|
||||||
candidates = detector.detect_duplicates(entities)
|
|
||||||
print(f" Found {len(candidates)} duplicate candidate(s)")
|
|
||||||
for candidate in candidates:
|
|
||||||
print(f" {candidate.entity1['name']} <-> {candidate.entity2['name']}")
|
|
||||||
print(f" Similarity: {candidate.similarity_score:.3f}, Confidence: {candidate.confidence:.3f}")
|
|
||||||
|
|
||||||
# Detect duplicate groups
|
|
||||||
print(" Running group detection...")
|
|
||||||
duplicate_groups = detector.detect_duplicate_groups(entities)
|
|
||||||
print(f" Found {len(duplicate_groups)} duplicate group(s)")
|
|
||||||
for i, group in enumerate(duplicate_groups, 1):
|
|
||||||
names = [e['name'] for e in group.entities]
|
|
||||||
print(f" Group {i}: {names} (confidence: {group.confidence:.3f})")
|
|
||||||
|
|
||||||
# Incremental detection
|
|
||||||
print(" Running incremental detection...")
|
|
||||||
existing_entities = entities[:3]
|
|
||||||
new_entities = entities[3:]
|
|
||||||
incremental_candidates = detector.incremental_detect(new_entities, existing_entities, threshold=0.7)
|
|
||||||
print(f" Found {len(incremental_candidates)} incremental duplicate(s)")
|
|
||||||
for candidate in incremental_candidates:
|
|
||||||
print(f" {candidate.entity1['name']} duplicates {candidate.entity2['name']} (confidence: {candidate.confidence:.3f})")
|
|
||||||
|
|
||||||
# Example: Similarity Calculation
|
|
||||||
print("\n[4] Testing Similarity Calculation...")
|
|
||||||
|
|
||||||
# Initialize SimilarityCalculator
|
|
||||||
calculator = SimilarityCalculator(
|
|
||||||
string_weight=0.4,
|
|
||||||
property_weight=0.3,
|
|
||||||
relationship_weight=0.2,
|
|
||||||
embedding_weight=0.1,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate overall similarity (multi-factor)
|
|
||||||
entity1, entity2 = entities[0], entities[1]
|
|
||||||
result = calculator.calculate_similarity(entity1, entity2)
|
|
||||||
print(f" Overall Similarity: {result.score:.3f}")
|
|
||||||
print(f" Components: {result.components}")
|
|
||||||
|
|
||||||
# String similarity methods
|
|
||||||
str1, str2 = "Apple Inc.", "Apple Inc"
|
|
||||||
for method in ["levenshtein", "jaro_winkler", "cosine"]:
|
|
||||||
score = calculator.calculate_string_similarity(str1, str2, method=method)
|
|
||||||
print(f" {method}: {score:.3f}")
|
|
||||||
|
|
||||||
# Property and relationship similarity
|
|
||||||
prop_score = calculator.calculate_property_similarity(entity1, entity2)
|
|
||||||
rel_score = calculator.calculate_relationship_similarity(entity1, entity2)
|
|
||||||
print(f" Property Similarity: {prop_score:.3f}")
|
|
||||||
print(f" Relationship Similarity: {rel_score:.3f}")
|
|
||||||
|
|
||||||
# Batch similarity calculation
|
|
||||||
similarity_pairs = calculator.batch_calculate_similarity(entities, threshold=0.5)
|
|
||||||
print(f" Found {len(similarity_pairs)} similar pairs (threshold >= 0.5)")
|
|
||||||
for e1, e2, score in similarity_pairs:
|
|
||||||
print(f" {e1['name']} <-> {e2['name']}: {score:.3f}")
|
|
||||||
|
|
||||||
# Example: Entity Merging
|
|
||||||
print("\n[5] Testing Entity Merging...")
|
|
||||||
|
|
||||||
# Initialize EntityMerger
|
|
||||||
merger = EntityMerger(preserve_provenance=True)
|
|
||||||
|
|
||||||
# Merge duplicates (automatic detection)
|
|
||||||
print(" Merging duplicates (automatic)...")
|
|
||||||
merge_operations = merger.merge_duplicates(entities)
|
|
||||||
print(f" Original entities: {len(entities)}")
|
|
||||||
print(f" Merge operations: {len(merge_operations)}")
|
|
||||||
for i, op in enumerate(merge_operations, 1):
|
|
||||||
print(f" Operation {i}: Merged {len(op.source_entities)} entities -> {op.merged_entity.get('name')}")
|
|
||||||
if op.merge_result.conflicts:
|
|
||||||
print(f" Conflicts: {len(op.merge_result.conflicts)}")
|
|
||||||
|
|
||||||
# Merge with specific strategy
|
|
||||||
print(" Merging with KEEP_MOST_COMPLETE strategy...")
|
|
||||||
operations = merger.merge_duplicates(entities, strategy=MergeStrategy.KEEP_MOST_COMPLETE)
|
|
||||||
print(f" Merged using KEEP_MOST_COMPLETE: {len(operations)} operations")
|
|
||||||
|
|
||||||
# Merge specific group
|
|
||||||
print(" Merging specific group...")
|
|
||||||
duplicate_entities = [entities[0], entities[1]]
|
|
||||||
operation = merger.merge_entity_group(duplicate_entities, strategy=MergeStrategy.KEEP_FIRST)
|
|
||||||
print(f" Merged group: {[e['name'] for e in operation.source_entities]} -> {operation.merged_entity['name']}")
|
|
||||||
|
|
||||||
# Get merge history
|
|
||||||
history = merger.get_merge_history()
|
|
||||||
print(f" Total merge operations in history: {len(history)}")
|
|
||||||
|
|
||||||
# Validate merge quality
|
|
||||||
if operations:
|
|
||||||
validation = merger.validate_merge_quality(operations[0])
|
|
||||||
print(f" Validation: Valid={validation['valid']}, Quality={validation['quality_score']:.3f}")
|
|
||||||
|
|
||||||
print("\n=== Deduplication Verification Completed Successfully ===")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
verify_deduplication_notebook()
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
"""
|
|
||||||
Script to verify the usage of the Semantica Knowledge Graph (KG) Module.
|
|
||||||
This simulates the typical usage pattern described in kg_usage.md.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Add project root to path
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
||||||
|
|
||||||
from semantica.kg import GraphBuilder, GraphAnalyzer, TemporalGraphQuery
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
||||||
logger = logging.getLogger("verify_kg")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Starting KG Module Verification...")
|
|
||||||
|
|
||||||
# --- Step 1: Build Knowledge Graph ---
|
|
||||||
print("\n--- Step 1: Graph Building ---")
|
|
||||||
|
|
||||||
# Define some source data with temporal info
|
|
||||||
sources = [
|
|
||||||
{
|
|
||||||
"entities": [
|
|
||||||
{"id": "e1", "name": "Alice", "type": "Person"},
|
|
||||||
{"id": "e2", "name": "Bob", "type": "Person"},
|
|
||||||
{"id": "e3", "name": "Semantica", "type": "Project"}
|
|
||||||
],
|
|
||||||
"relationships": [
|
|
||||||
{
|
|
||||||
"source": "e1", "target": "e2", "type": "knows",
|
|
||||||
"valid_from": "2023-01-01", "valid_until": None
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "e1", "target": "e3", "type": "works_on",
|
|
||||||
"valid_from": "2023-06-01", "valid_until": "2024-01-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "e2", "target": "e3", "type": "works_on",
|
|
||||||
"valid_from": "2024-01-01", "valid_until": None
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Initialize builder (disable complex features for simple verification)
|
|
||||||
builder = GraphBuilder(
|
|
||||||
merge_entities=False,
|
|
||||||
resolve_conflicts=False,
|
|
||||||
enable_temporal=True
|
|
||||||
)
|
|
||||||
|
|
||||||
kg = builder.build(sources)
|
|
||||||
logger.info(f"Graph built with {len(kg['entities'])} entities and {len(kg['relationships'])} relationships.")
|
|
||||||
|
|
||||||
# --- Step 2: Analyze Graph ---
|
|
||||||
logger.info("\n--- Step 2: Graph Analysis ---")
|
|
||||||
|
|
||||||
# Mocking sub-analyzers if they are not fully implemented or require external libs not present
|
|
||||||
# Assuming they are implemented or we can run with defaults.
|
|
||||||
# Note: GraphAnalyzer imports CentralityCalculator etc.
|
|
||||||
# If those modules have dependencies (like networkx), they need to be installed.
|
|
||||||
# Let's try to run it. If it fails, we know we need dependencies.
|
|
||||||
|
|
||||||
try:
|
|
||||||
analyzer = GraphAnalyzer()
|
|
||||||
# We might need to mock internal calls if they fail due to missing heavy libs in this environment
|
|
||||||
# But let's try.
|
|
||||||
# To avoid failure if CentralityCalculator fails, we can catch it.
|
|
||||||
# But for verification script, we want to see it run.
|
|
||||||
# Since I can't check installed packages easily without running pip list, I'll assume standard deps.
|
|
||||||
|
|
||||||
# However, to be safe and avoid script crash on things I haven't checked (like networkx),
|
|
||||||
# I will wrap in try-except block for analysis.
|
|
||||||
analysis = analyzer.analyze_graph(kg)
|
|
||||||
logger.info("Graph analysis completed.")
|
|
||||||
logger.info(f"Metrics: {json.dumps(analysis.get('metrics', {}), indent=2)}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Graph analysis skipped or failed: {e}")
|
|
||||||
|
|
||||||
# --- Step 3: Temporal Query ---
|
|
||||||
logger.info("\n--- Step 3: Temporal Querying ---")
|
|
||||||
|
|
||||||
query_engine = TemporalGraphQuery()
|
|
||||||
|
|
||||||
# Query at a specific time
|
|
||||||
at_time = "2023-08-01"
|
|
||||||
result = query_engine.query_at_time(kg, query="", at_time=at_time)
|
|
||||||
|
|
||||||
logger.info(f"Relationships active at {at_time}:")
|
|
||||||
for rel in result["relationships"]:
|
|
||||||
logger.info(f" {rel['source']} --[{rel['type']}]--> {rel['target']}")
|
|
||||||
|
|
||||||
# Verify expected results
|
|
||||||
# Alice knows Bob (from 2023-01-01) -> Active
|
|
||||||
# Alice works_on Semantica (from 2023-06-01 to 2024-01-01) -> Active
|
|
||||||
# Bob works_on Semantica (from 2024-01-01) -> Not Active
|
|
||||||
|
|
||||||
active_rels = len(result["relationships"])
|
|
||||||
logger.info(f"Found {active_rels} active relationships (Expected: 2).")
|
|
||||||
|
|
||||||
if active_rels == 2:
|
|
||||||
logger.info("✅ Temporal query verification successful!")
|
|
||||||
else:
|
|
||||||
logger.error("❌ Temporal query verification failed!")
|
|
||||||
|
|
||||||
logger.info("\n✅ KG Module Verification Completed!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -31,7 +31,7 @@ class LLMOntologyGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
base_uri = options.get("base_uri")
|
base_uri = options.get("base_uri")
|
||||||
name = options.get("name") or "GeneratedOntology"
|
name = options.get("name")
|
||||||
version = options.get("version") or "1.0"
|
version = options.get("version") or "1.0"
|
||||||
|
|
||||||
prompt = self._build_prompt(text=text, name=name, base_uri=base_uri)
|
prompt = self._build_prompt(text=text, name=name, base_uri=base_uri)
|
||||||
|
|||||||
@@ -216,6 +216,10 @@ class NamespaceManager:
|
|||||||
|
|
||||||
def _to_camel_case(self, name: str) -> str:
|
def _to_camel_case(self, name: str) -> str:
|
||||||
"""Convert name to camelCase."""
|
"""Convert name to camelCase."""
|
||||||
|
# Check if already likely camelCase (starts with lower, has upper, single word)
|
||||||
|
if name and name[0].islower() and any(c.isupper() for c in name) and ' ' not in name and '_' not in name:
|
||||||
|
return name
|
||||||
|
|
||||||
# Remove special characters and split
|
# Remove special characters and split
|
||||||
words = re.findall(r"[a-zA-Z0-9]+", name)
|
words = re.findall(r"[a-zA-Z0-9]+", name)
|
||||||
if not words:
|
if not words:
|
||||||
|
|||||||
@@ -262,8 +262,8 @@ class NamingConventions:
|
|||||||
# camelCase for object properties
|
# camelCase for object properties
|
||||||
suggested = self._to_camel_case(name)
|
suggested = self._to_camel_case(name)
|
||||||
else:
|
else:
|
||||||
# lowercase for data properties
|
# camelCase for data properties as well (standard practice)
|
||||||
suggested = name.lower()
|
suggested = self._to_camel_case(name)
|
||||||
|
|
||||||
return suggested
|
return suggested
|
||||||
|
|
||||||
@@ -364,6 +364,10 @@ class NamingConventions:
|
|||||||
|
|
||||||
def _to_camel_case(self, name: str) -> str:
|
def _to_camel_case(self, name: str) -> str:
|
||||||
"""Convert to camelCase."""
|
"""Convert to camelCase."""
|
||||||
|
# Check if already likely camelCase (starts with lower, has upper, single word)
|
||||||
|
if name and name[0].islower() and any(c.isupper() for c in name) and ' ' not in name and '_' not in name:
|
||||||
|
return name
|
||||||
|
|
||||||
words = re.findall(r"[a-zA-Z0-9]+", name)
|
words = re.findall(r"[a-zA-Z0-9]+", name)
|
||||||
if not words:
|
if not words:
|
||||||
return "hasProperty"
|
return "hasProperty"
|
||||||
@@ -381,8 +385,8 @@ class NamingConventions:
|
|||||||
# Basic singularization rules
|
# Basic singularization rules
|
||||||
if name.lower().endswith("ies"):
|
if name.lower().endswith("ies"):
|
||||||
return name[:-3] + "y"
|
return name[:-3] + "y"
|
||||||
elif name.lower().endswith("es"):
|
elif name.lower().endswith("es") and not name.lower().endswith("ss"):
|
||||||
return name[:-2]
|
return name[:-2]
|
||||||
elif name.lower().endswith("s") and len(name) > 1:
|
elif name.lower().endswith("s") and len(name) > 1 and not name.lower().endswith("ss") and name.lower() not in ["class", "process", "analysis"]:
|
||||||
return name[:-1]
|
return name[:-1]
|
||||||
return name
|
return name
|
||||||
|
|||||||
@@ -170,7 +170,13 @@ class OntologyGenerator:
|
|||||||
self.progress_tracker.update_tracking(
|
self.progress_tracker.update_tracking(
|
||||||
tracking_id, message="Stage 3: Mapping to OWL types..."
|
tracking_id, message="Stage 3: Mapping to OWL types..."
|
||||||
)
|
)
|
||||||
typed_definitions = self._stage3_definition_to_types(definitions, **options)
|
|
||||||
|
# Ensure entities and relationships are available for property inference
|
||||||
|
stage3_options = options.copy()
|
||||||
|
stage3_options["entities"] = data.get("entities", [])
|
||||||
|
stage3_options["relationships"] = data.get("relationships", [])
|
||||||
|
|
||||||
|
typed_definitions = self._stage3_definition_to_types(definitions, **stage3_options)
|
||||||
|
|
||||||
# Stage 4: Hierarchy Generation
|
# Stage 4: Hierarchy Generation
|
||||||
self.progress_tracker.update_tracking(
|
self.progress_tracker.update_tracking(
|
||||||
@@ -266,9 +272,14 @@ class OntologyGenerator:
|
|||||||
relationships = options.get("relationships", [])
|
relationships = options.get("relationships", [])
|
||||||
entities = options.get("entities", [])
|
entities = options.get("entities", [])
|
||||||
|
|
||||||
|
# Clean options for infer_properties to avoid multiple values for arguments
|
||||||
|
prop_options = options.copy()
|
||||||
|
prop_options.pop("entities", None)
|
||||||
|
prop_options.pop("relationships", None)
|
||||||
|
|
||||||
# Infer properties
|
# Infer properties
|
||||||
properties = self.property_generator.infer_properties(
|
properties = self.property_generator.infer_properties(
|
||||||
entities=entities, relationships=relationships, classes=classes, **options
|
entities=entities, relationships=relationships, classes=classes, **prop_options
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add types to classes
|
# Add types to classes
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ class PropertyGenerator:
|
|||||||
submodule="PropertyGenerator",
|
submodule="PropertyGenerator",
|
||||||
message=f"Inferring properties from {len(entities)} entities and {len(relationships)} relationships",
|
message=f"Inferring properties from {len(entities)} entities and {len(relationships)} relationships",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Merge config into options
|
||||||
|
for key, value in self.config.items():
|
||||||
|
if key not in options:
|
||||||
|
options[key] = value
|
||||||
|
|
||||||
try:
|
try:
|
||||||
properties = []
|
properties = []
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from bs4 import BeautifulSoup
|
|||||||
|
|
||||||
from ..utils.exceptions import ProcessingError, ValidationError
|
from ..utils.exceptions import ProcessingError, ValidationError
|
||||||
from ..utils.logging import get_logger
|
from ..utils.logging import get_logger
|
||||||
|
from ..utils.progress_tracker import get_progress_tracker
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -65,6 +66,20 @@ class HTMLElement:
|
|||||||
children: List["HTMLElement"] = field(default_factory=list)
|
children: List["HTMLElement"] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HTMLData:
|
||||||
|
"""HTML document representation."""
|
||||||
|
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
text: str
|
||||||
|
html: str
|
||||||
|
links: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
images: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
forms: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
tables: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
structure: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class HTMLParser:
|
class HTMLParser:
|
||||||
"""HTML document parser."""
|
"""HTML document parser."""
|
||||||
|
|
||||||
@@ -81,7 +96,7 @@ class HTMLParser:
|
|||||||
|
|
||||||
def parse(
|
def parse(
|
||||||
self, html_content: Union[str, Path], base_url: Optional[str] = None, **options
|
self, html_content: Union[str, Path], base_url: Optional[str] = None, **options
|
||||||
) -> Dict[str, Any]:
|
) -> HTMLData:
|
||||||
"""
|
"""
|
||||||
Parse HTML content.
|
Parse HTML content.
|
||||||
|
|
||||||
@@ -96,7 +111,7 @@ class HTMLParser:
|
|||||||
- clean_text: Whether to clean extracted text (default: True)
|
- clean_text: Whether to clean extracted text (default: True)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Parsed HTML data
|
HTMLData: Parsed HTML data
|
||||||
"""
|
"""
|
||||||
# Track HTML parsing
|
# Track HTML parsing
|
||||||
file_path = None
|
file_path = None
|
||||||
@@ -160,16 +175,16 @@ class HTMLParser:
|
|||||||
status="completed",
|
status="completed",
|
||||||
message=f"Parsed HTML: {len(links)} links, {len(images)} images",
|
message=f"Parsed HTML: {len(links)} links, {len(images)} images",
|
||||||
)
|
)
|
||||||
return {
|
return HTMLData(
|
||||||
"metadata": metadata.__dict__,
|
metadata=metadata.__dict__,
|
||||||
"text": text,
|
text=text,
|
||||||
"html": html_string,
|
html=html_string,
|
||||||
"links": links,
|
links=links,
|
||||||
"images": images,
|
images=images,
|
||||||
"forms": forms,
|
forms=forms,
|
||||||
"tables": tables,
|
tables=tables,
|
||||||
"structure": structure,
|
structure=structure,
|
||||||
}
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.progress_tracker.stop_tracking(
|
self.progress_tracker.stop_tracking(
|
||||||
@@ -184,6 +199,26 @@ class HTMLParser:
|
|||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def extract_metadata(self, html_content: Union[str, Path]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract metadata from HTML.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
html_content: HTML content or file path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Extracted metadata
|
||||||
|
"""
|
||||||
|
result = self.parse(
|
||||||
|
html_content,
|
||||||
|
extract_links=False,
|
||||||
|
extract_images=False,
|
||||||
|
extract_forms=False,
|
||||||
|
extract_tables=False,
|
||||||
|
clean_text=False,
|
||||||
|
)
|
||||||
|
return result.metadata
|
||||||
|
|
||||||
def extract_text(self, html_content: Union[str, Path], clean: bool = True) -> str:
|
def extract_text(self, html_content: Union[str, Path], clean: bool = True) -> str:
|
||||||
"""
|
"""
|
||||||
Extract text from HTML.
|
Extract text from HTML.
|
||||||
@@ -203,7 +238,7 @@ class HTMLParser:
|
|||||||
extract_tables=False,
|
extract_tables=False,
|
||||||
clean_text=clean,
|
clean_text=clean,
|
||||||
)
|
)
|
||||||
return result["text"]
|
return result.text
|
||||||
|
|
||||||
def extract_links(
|
def extract_links(
|
||||||
self, html_content: Union[str, Path], base_url: Optional[str] = None
|
self, html_content: Union[str, Path], base_url: Optional[str] = None
|
||||||
@@ -225,7 +260,7 @@ class HTMLParser:
|
|||||||
extract_forms=False,
|
extract_forms=False,
|
||||||
extract_tables=False,
|
extract_tables=False,
|
||||||
)
|
)
|
||||||
return result["links"]
|
return result.links
|
||||||
|
|
||||||
def _extract_metadata(self, soup: BeautifulSoup) -> HTMLMetadata:
|
def _extract_metadata(self, soup: BeautifulSoup) -> HTMLMetadata:
|
||||||
"""Extract metadata from HTML."""
|
"""Extract metadata from HTML."""
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ class StructuredDataParser:
|
|||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
self.config.update(kwargs)
|
self.config.update(kwargs)
|
||||||
|
|
||||||
|
# Initialize progress tracker
|
||||||
|
self.progress_tracker = get_progress_tracker()
|
||||||
|
|
||||||
# Initialize parsers
|
# Initialize parsers
|
||||||
self.json_parser = JSONParser(**self.config.get("json", {}))
|
self.json_parser = JSONParser(**self.config.get("json", {}))
|
||||||
self.csv_parser = CSVParser(**self.config.get("csv", {}))
|
self.csv_parser = CSVParser(**self.config.get("csv", {}))
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ License: MIT
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
import re
|
||||||
|
|
||||||
from ..utils.exceptions import ProcessingError, ValidationError
|
from ..utils.exceptions import ProcessingError, ValidationError
|
||||||
from ..utils.logging import get_logger
|
from ..utils.logging import get_logger
|
||||||
@@ -203,9 +204,58 @@ class AbductiveReasoner:
|
|||||||
|
|
||||||
def _rule_explains_observation(self, rule: Rule, observation: Observation) -> bool:
|
def _rule_explains_observation(self, rule: Rule, observation: Observation) -> bool:
|
||||||
"""Check if rule can explain observation."""
|
"""Check if rule can explain observation."""
|
||||||
# Simple check: rule conclusion matches observation
|
# Check if rule conclusion matches observation description
|
||||||
# Can be enhanced with more sophisticated matching
|
# Try exact match first
|
||||||
return True
|
if rule.conclusion == observation.description:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Try unification if variables are involved
|
||||||
|
if "?" in rule.conclusion:
|
||||||
|
bindings = self._unify(rule.conclusion, observation.description, {})
|
||||||
|
if bindings is not None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _parse_predicate(self, text: str) -> tuple[str, List[str]]:
|
||||||
|
"""Parse 'Predicate(arg1, arg2)' into ('Predicate', ['arg1', 'arg2'])."""
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return text, []
|
||||||
|
match = re.match(r"(\w+)\((.+)\)", text)
|
||||||
|
if not match:
|
||||||
|
return text, []
|
||||||
|
predicate = match.group(1)
|
||||||
|
args = [arg.strip() for arg in match.group(2).split(",")]
|
||||||
|
return predicate, args
|
||||||
|
|
||||||
|
def _unify(self, condition: str, fact: str, bindings: Dict[str, str]) -> Optional[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Try to unify a condition (with vars) against a fact.
|
||||||
|
Returns new bindings if successful, None otherwise.
|
||||||
|
"""
|
||||||
|
if condition == fact:
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
cond_pred, cond_args = self._parse_predicate(condition)
|
||||||
|
fact_pred, fact_args = self._parse_predicate(fact)
|
||||||
|
|
||||||
|
if cond_pred != fact_pred:
|
||||||
|
return None
|
||||||
|
if len(cond_args) != len(fact_args):
|
||||||
|
return None
|
||||||
|
|
||||||
|
new_bindings = bindings.copy()
|
||||||
|
for c_arg, f_arg in zip(cond_args, fact_args):
|
||||||
|
if c_arg.startswith("?"):
|
||||||
|
if c_arg in new_bindings:
|
||||||
|
if new_bindings[c_arg] != f_arg:
|
||||||
|
return None # Conflict
|
||||||
|
else:
|
||||||
|
new_bindings[c_arg] = f_arg
|
||||||
|
else:
|
||||||
|
if c_arg != f_arg:
|
||||||
|
return None # Constant mismatch
|
||||||
|
return new_bindings
|
||||||
|
|
||||||
def _calculate_coverage(self, rule: Rule, observation: Observation) -> float:
|
def _calculate_coverage(self, rule: Rule, observation: Observation) -> float:
|
||||||
"""Calculate how well rule covers observation."""
|
"""Calculate how well rule covers observation."""
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ License: MIT
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, List, Optional, Set
|
from typing import Any, Dict, List, Optional, Set
|
||||||
|
import re
|
||||||
|
|
||||||
from ..utils.exceptions import ProcessingError, ValidationError
|
from ..utils.exceptions import ProcessingError, ValidationError
|
||||||
from ..utils.logging import get_logger
|
from ..utils.logging import get_logger
|
||||||
@@ -148,12 +149,16 @@ class DeductiveReasoner:
|
|||||||
rules = self.rule_manager.get_all_rules()
|
rules = self.rule_manager.get_all_rules()
|
||||||
|
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
# Check if rule can be applied
|
# Find all matches (bindings) for the rule
|
||||||
if self._can_apply_rule(rule, premises):
|
matches = self._find_matches(rule.conditions, {})
|
||||||
conclusion = self._apply_rule_to_premises(rule, premises)
|
|
||||||
|
for bindings in matches:
|
||||||
|
conclusion = self._apply_rule_to_premises(rule, premises, bindings)
|
||||||
if conclusion:
|
if conclusion:
|
||||||
conclusions.append(conclusion)
|
# Check if conclusion is new (not in known facts)
|
||||||
self.known_facts.add(conclusion.statement)
|
if conclusion.statement not in self.known_facts:
|
||||||
|
conclusions.append(conclusion)
|
||||||
|
self.known_facts.add(conclusion.statement)
|
||||||
|
|
||||||
self.progress_tracker.stop_tracking(
|
self.progress_tracker.stop_tracking(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
@@ -168,39 +173,127 @@ class DeductiveReasoner:
|
|||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _can_apply_rule(self, rule: Rule, premises: List[Premise]) -> bool:
|
def _parse_predicate(self, text: str) -> tuple[str, List[str]]:
|
||||||
"""Check if rule can be applied to premises."""
|
"""Parse 'Predicate(arg1, arg2)' into ('Predicate', ['arg1', 'arg2'])."""
|
||||||
# Check if all rule conditions match premises
|
if not isinstance(text, str):
|
||||||
premise_statements = {p.statement for p in premises}
|
return text, []
|
||||||
|
match = re.match(r"(\w+)\((.+)\)", text)
|
||||||
|
if not match:
|
||||||
|
return text, []
|
||||||
|
predicate = match.group(1)
|
||||||
|
args = [arg.strip() for arg in match.group(2).split(",")]
|
||||||
|
return predicate, args
|
||||||
|
|
||||||
for condition in rule.conditions:
|
def _unify(self, condition: str, fact: str, bindings: Dict[str, str]) -> Optional[Dict[str, str]]:
|
||||||
if (
|
"""
|
||||||
condition not in premise_statements
|
Try to unify a condition (with vars) against a fact.
|
||||||
and condition not in self.known_facts
|
Returns new bindings if successful, None otherwise.
|
||||||
):
|
"""
|
||||||
return False
|
if condition == fact:
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
cond_pred, cond_args = self._parse_predicate(condition)
|
||||||
|
fact_pred, fact_args = self._parse_predicate(fact)
|
||||||
|
|
||||||
|
if cond_pred != fact_pred:
|
||||||
|
return None
|
||||||
|
if len(cond_args) != len(fact_args):
|
||||||
|
return None
|
||||||
|
|
||||||
|
new_bindings = bindings.copy()
|
||||||
|
for c_arg, f_arg in zip(cond_args, fact_args):
|
||||||
|
if c_arg.startswith("?"):
|
||||||
|
if c_arg in new_bindings:
|
||||||
|
if new_bindings[c_arg] != f_arg:
|
||||||
|
return None # Conflict
|
||||||
|
else:
|
||||||
|
new_bindings[c_arg] = f_arg
|
||||||
|
else:
|
||||||
|
if c_arg != f_arg:
|
||||||
|
return None # Constant mismatch
|
||||||
|
return new_bindings
|
||||||
|
|
||||||
return True
|
def _substitute_bindings(self, text: str, bindings: Dict[str, str]) -> str:
|
||||||
|
"""Substitute variables in text with bindings."""
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return text
|
||||||
|
pred, args = self._parse_predicate(text)
|
||||||
|
if not args:
|
||||||
|
return text
|
||||||
|
|
||||||
|
new_args = []
|
||||||
|
for arg in args:
|
||||||
|
if arg in bindings:
|
||||||
|
new_args.append(bindings[arg])
|
||||||
|
else:
|
||||||
|
new_args.append(arg)
|
||||||
|
|
||||||
|
return f"{pred}({', '.join(new_args)})"
|
||||||
|
|
||||||
|
def _find_matches(self, conditions: List[str], bindings: Dict[str, str]) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Recursively find all bindings that satisfy the conditions.
|
||||||
|
"""
|
||||||
|
if not conditions:
|
||||||
|
return [bindings]
|
||||||
|
|
||||||
|
first = conditions[0]
|
||||||
|
# Substitute current bindings into first condition before matching
|
||||||
|
first_substituted = self._substitute_bindings(first, bindings)
|
||||||
|
rest = conditions[1:]
|
||||||
|
|
||||||
|
valid_bindings = []
|
||||||
|
|
||||||
|
# Try to match 'first' against all known facts
|
||||||
|
for fact in self.known_facts:
|
||||||
|
# Skip if fact is not a string (unhashable/objects) for now
|
||||||
|
if not isinstance(fact, str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
unified = self._unify(first_substituted, fact, bindings)
|
||||||
|
if unified is not None:
|
||||||
|
# Recursive step
|
||||||
|
results = self._find_matches(rest, unified)
|
||||||
|
valid_bindings.extend(results)
|
||||||
|
|
||||||
|
return valid_bindings
|
||||||
|
|
||||||
def _apply_rule_to_premises(
|
def _apply_rule_to_premises(
|
||||||
self, rule: Rule, premises: List[Premise]
|
self, rule: Rule, premises: List[Premise], bindings: Dict[str, str]
|
||||||
) -> Optional[Conclusion]:
|
) -> Optional[Conclusion]:
|
||||||
"""Apply rule to premises and generate conclusion."""
|
"""Apply rule to premises and generate conclusion."""
|
||||||
# Find matching premises
|
# Find matching premises (those that support the bindings)
|
||||||
matching_premises = [
|
# This is a bit approximate, ideally we track which premise supported which condition
|
||||||
p
|
matching_premises = []
|
||||||
for p in premises
|
|
||||||
if p.statement in rule.conditions or p.statement in self.known_facts
|
# Instantiate conclusion
|
||||||
]
|
conclusion_stmt = rule.conclusion
|
||||||
|
if bindings:
|
||||||
|
conclusion_stmt = self._substitute_bindings(conclusion_stmt, bindings)
|
||||||
|
|
||||||
|
# Find premises that match the conditions (instantiated)
|
||||||
|
for cond in rule.conditions:
|
||||||
|
instantiated = self._substitute_bindings(cond, bindings)
|
||||||
|
for p in premises:
|
||||||
|
if p.statement == instantiated:
|
||||||
|
matching_premises.append(p)
|
||||||
|
break
|
||||||
|
# Note: some conditions might be matched by self.known_facts which are not in 'premises' arg
|
||||||
|
# but are in self.known_facts.
|
||||||
|
# If a premise is not in the passed list but in known_facts, we can't add it to matching_premises list
|
||||||
|
# unless we find the Premise object.
|
||||||
|
# But known_facts stores strings.
|
||||||
|
# So matching_premises might be incomplete if we rely on known_facts.
|
||||||
|
# However, for this method signature, we return a Conclusion with premises.
|
||||||
|
|
||||||
conclusion = Conclusion(
|
conclusion = Conclusion(
|
||||||
conclusion_id=f"conc_{len(matching_premises)}",
|
conclusion_id=f"conc_{rule.name}_{len(matching_premises)}",
|
||||||
statement=rule.conclusion,
|
statement=conclusion_stmt,
|
||||||
premises=matching_premises,
|
premises=matching_premises,
|
||||||
rule_applied=rule,
|
rule_applied=rule,
|
||||||
confidence=rule.confidence,
|
confidence=rule.confidence,
|
||||||
proof_steps=[f"Applied rule: {rule.name}"],
|
proof_steps=[f"Applied rule: {rule.name} with bindings {bindings}"],
|
||||||
metadata={"rule_id": rule.rule_id},
|
metadata={"rule_id": rule.rule_id, "bindings": bindings},
|
||||||
)
|
)
|
||||||
|
|
||||||
return conclusion
|
return conclusion
|
||||||
@@ -272,6 +365,7 @@ class DeductiveReasoner:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Check if goal is already known
|
# Check if goal is already known
|
||||||
|
# Try direct match
|
||||||
if goal in self.known_facts:
|
if goal in self.known_facts:
|
||||||
return Conclusion(
|
return Conclusion(
|
||||||
conclusion_id=f"known_{goal}",
|
conclusion_id=f"known_{goal}",
|
||||||
@@ -279,35 +373,65 @@ class DeductiveReasoner:
|
|||||||
confidence=1.0,
|
confidence=1.0,
|
||||||
proof_steps=["Known fact"],
|
proof_steps=["Known fact"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Try unification with known facts
|
||||||
|
if isinstance(goal, str) and "?" in goal:
|
||||||
|
for fact in self.known_facts:
|
||||||
|
if isinstance(fact, str):
|
||||||
|
if self._unify(goal, fact, {}) is not None:
|
||||||
|
return Conclusion(
|
||||||
|
conclusion_id=f"known_{fact}",
|
||||||
|
statement=fact,
|
||||||
|
confidence=1.0,
|
||||||
|
proof_steps=[f"Known fact (matched pattern {goal})"],
|
||||||
|
)
|
||||||
|
|
||||||
# Find rules that can prove goal
|
# Find rules that can prove goal
|
||||||
rules = self.rule_manager.get_all_rules()
|
rules = self.rule_manager.get_all_rules()
|
||||||
applicable_rules = [r for r in rules if r.conclusion == goal]
|
|
||||||
|
# Use unified matching for finding applicable rules
|
||||||
|
applicable_rules_and_bindings = []
|
||||||
|
for r in rules:
|
||||||
|
bindings = self._unify(r.conclusion, goal, {})
|
||||||
|
if bindings is not None:
|
||||||
|
applicable_rules_and_bindings.append((r, bindings))
|
||||||
|
|
||||||
for rule in applicable_rules:
|
for rule, initial_bindings in applicable_rules_and_bindings:
|
||||||
# Try to prove all premises
|
# Try to prove all premises
|
||||||
premise_conclusions = []
|
premise_conclusions = []
|
||||||
all_proven = True
|
all_proven = True
|
||||||
|
current_bindings = initial_bindings.copy()
|
||||||
|
|
||||||
for condition in rule.conditions:
|
for condition in rule.conditions:
|
||||||
|
# Instantiate condition with current bindings
|
||||||
|
instantiated_cond = self._substitute_bindings(condition, current_bindings)
|
||||||
|
|
||||||
premise_conclusion = self._prove_backward(
|
premise_conclusion = self._prove_backward(
|
||||||
condition, proof, depth + 1, max_depth, **options
|
instantiated_cond, proof, depth + 1, max_depth, **options
|
||||||
)
|
)
|
||||||
if premise_conclusion:
|
if premise_conclusion:
|
||||||
premise_conclusions.append(premise_conclusion)
|
premise_conclusions.append(premise_conclusion)
|
||||||
|
# Update bindings if we proved something more specific
|
||||||
|
new_bindings = self._unify(instantiated_cond, premise_conclusion.statement, current_bindings)
|
||||||
|
if new_bindings:
|
||||||
|
current_bindings = new_bindings
|
||||||
else:
|
else:
|
||||||
all_proven = False
|
all_proven = False
|
||||||
break
|
break
|
||||||
|
|
||||||
if all_proven:
|
if all_proven:
|
||||||
# All premises proven, rule can fire
|
# All premises proven, rule can fire
|
||||||
|
# Instantiate conclusion with final bindings
|
||||||
|
final_conclusion = self._substitute_bindings(rule.conclusion, current_bindings)
|
||||||
|
|
||||||
conclusion = Conclusion(
|
conclusion = Conclusion(
|
||||||
conclusion_id=f"conc_{goal}",
|
conclusion_id=f"conc_{goal}",
|
||||||
statement=goal,
|
statement=final_conclusion,
|
||||||
premises=[Premise(p, p) for p in rule.conditions],
|
premises=[p for p in premise_conclusions], # Use actual premises found
|
||||||
rule_applied=rule,
|
rule_applied=rule,
|
||||||
confidence=rule.confidence,
|
confidence=rule.confidence,
|
||||||
proof_steps=[f"Proved using rule: {rule.name}"],
|
proof_steps=[f"Proved using rule: {rule.name} with bindings {current_bindings}"],
|
||||||
|
metadata={"rule_id": rule.rule_id, "bindings": current_bindings}
|
||||||
)
|
)
|
||||||
return conclusion
|
return conclusion
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Author: Semantica Contributors
|
|||||||
License: MIT
|
License: MIT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable, Dict, List, Optional, Set
|
from typing import Any, Callable, Dict, List, Optional, Set
|
||||||
@@ -194,10 +195,12 @@ class InferenceEngine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
# Check if rule can fire
|
# Find all matches for the rule
|
||||||
if self._can_rule_fire(rule):
|
matches = self._find_matches(rule.conditions, {})
|
||||||
# Apply rule
|
|
||||||
result = self._apply_rule(rule)
|
for bindings in matches:
|
||||||
|
# Apply rule with bindings
|
||||||
|
result = self._apply_rule(rule, bindings=bindings)
|
||||||
if result:
|
if result:
|
||||||
# Only consider it a new inference if the fact wasn't already known
|
# Only consider it a new inference if the fact wasn't already known
|
||||||
if self.add_fact(result.conclusion):
|
if self.add_fact(result.conclusion):
|
||||||
@@ -244,47 +247,83 @@ class InferenceEngine:
|
|||||||
tracking_id, message="Checking if goal is already a fact..."
|
tracking_id, message="Checking if goal is already a fact..."
|
||||||
)
|
)
|
||||||
|
|
||||||
is_fact = False
|
# Check for direct match or unification with facts
|
||||||
|
found_fact = None
|
||||||
|
|
||||||
|
# First try direct match (fastest)
|
||||||
try:
|
try:
|
||||||
if goal in self.facts:
|
if goal in self.facts:
|
||||||
is_fact = True
|
found_fact = goal
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if goal in self.unhashable_facts:
|
if goal in self.unhashable_facts:
|
||||||
is_fact = True
|
found_fact = goal
|
||||||
|
|
||||||
if is_fact:
|
# If not found and goal looks like a pattern (string with ?), try unification
|
||||||
|
if found_fact is None and isinstance(goal, str) and "?" in goal:
|
||||||
|
for fact in self.facts:
|
||||||
|
if isinstance(fact, str):
|
||||||
|
# Try to unify to see if it matches
|
||||||
|
if self._unify(goal, fact, {}) is not None:
|
||||||
|
found_fact = fact
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_fact:
|
||||||
self.progress_tracker.stop_tracking(
|
self.progress_tracker.stop_tracking(
|
||||||
tracking_id, status="completed", message="Goal is already a fact"
|
tracking_id, status="completed", message=f"Goal proven by fact: {found_fact}"
|
||||||
)
|
)
|
||||||
return InferenceResult(conclusion=goal, confidence=1.0)
|
return InferenceResult(conclusion=found_fact, confidence=1.0)
|
||||||
|
|
||||||
# Find rules that can prove the goal
|
# Find rules that can prove the goal
|
||||||
self.progress_tracker.update_tracking(
|
self.progress_tracker.update_tracking(
|
||||||
tracking_id, message="Finding rules that can prove the goal..."
|
tracking_id, message="Finding rules that can prove the goal..."
|
||||||
)
|
)
|
||||||
rules = self.rule_manager.get_all_rules()
|
rules = self.rule_manager.get_all_rules()
|
||||||
applicable_rules = [r for r in rules if self._rule_concludes(r, goal)]
|
|
||||||
|
# Use unified matching for finding applicable rules
|
||||||
|
applicable_rules_and_bindings = []
|
||||||
|
for r in rules:
|
||||||
|
bindings = self._unify(r.conclusion, goal, {})
|
||||||
|
if bindings is not None:
|
||||||
|
applicable_rules_and_bindings.append((r, bindings))
|
||||||
|
|
||||||
self.progress_tracker.update_tracking(
|
self.progress_tracker.update_tracking(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
message=f"Found {len(applicable_rules)} applicable rules, trying to prove premises...",
|
message=f"Found {len(applicable_rules_and_bindings)} applicable rules, trying to prove premises...",
|
||||||
)
|
)
|
||||||
for rule in applicable_rules:
|
|
||||||
# Try to prove premises
|
for rule, initial_bindings in applicable_rules_and_bindings:
|
||||||
premises = []
|
# Try to prove premises with bindings, propagating bindings between premises
|
||||||
|
current_bindings = initial_bindings.copy()
|
||||||
|
premises_results = []
|
||||||
all_premises_proven = True
|
all_premises_proven = True
|
||||||
|
|
||||||
for premise in rule.conditions:
|
for cond in rule.conditions:
|
||||||
premise_result = self.backward_chain(premise, **options)
|
# Instantiate condition with current bindings
|
||||||
|
instantiated_cond = self._substitute_bindings(cond, current_bindings)
|
||||||
|
|
||||||
|
# Recursively prove this condition
|
||||||
|
premise_result = self.backward_chain(instantiated_cond, **options)
|
||||||
|
|
||||||
if premise_result:
|
if premise_result:
|
||||||
premises.append(premise_result.conclusion)
|
premises_results.append(premise_result.conclusion)
|
||||||
|
|
||||||
|
# If the premise had variables, update bindings based on the proven fact
|
||||||
|
# We unify the instantiated condition (which might still have vars) with the proven conclusion
|
||||||
|
new_bindings = self._unify(instantiated_cond, premise_result.conclusion, current_bindings)
|
||||||
|
if new_bindings is not None:
|
||||||
|
current_bindings = new_bindings
|
||||||
|
else:
|
||||||
|
# This implies a conflict, which shouldn't happen if backward_chain returned success
|
||||||
|
# on instantiated_cond, but good to be safe
|
||||||
|
all_premises_proven = False
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
all_premises_proven = False
|
all_premises_proven = False
|
||||||
break
|
break
|
||||||
|
|
||||||
if all_premises_proven:
|
if all_premises_proven:
|
||||||
# All premises proven, rule can fire
|
# All premises proven, rule can fire
|
||||||
result = self._apply_rule(rule, premises)
|
result = self._apply_rule(rule, premises=premises_results, bindings=current_bindings)
|
||||||
if result:
|
if result:
|
||||||
self.progress_tracker.stop_tracking(
|
self.progress_tracker.stop_tracking(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
@@ -304,38 +343,115 @@ class InferenceEngine:
|
|||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _can_rule_fire(self, rule: Rule) -> bool:
|
def _parse_predicate(self, text: str) -> tuple[str, List[str]]:
|
||||||
"""Check if rule can fire (all conditions met)."""
|
"""Parse 'Predicate(arg1, arg2)' into ('Predicate', ['arg1', 'arg2'])."""
|
||||||
for condition in rule.conditions:
|
if not isinstance(text, str):
|
||||||
try:
|
return text, []
|
||||||
if condition not in self.facts:
|
match = re.match(r"(\w+)\((.+)\)", text)
|
||||||
return False
|
if not match:
|
||||||
except TypeError:
|
return text, []
|
||||||
if condition not in self.unhashable_facts:
|
predicate = match.group(1)
|
||||||
return False
|
args = [arg.strip() for arg in match.group(2).split(",")]
|
||||||
return True
|
return predicate, args
|
||||||
|
|
||||||
def _rule_concludes(self, rule: Rule, goal: Any) -> bool:
|
def _unify(self, condition: str, fact: str, bindings: Dict[str, str]) -> Optional[Dict[str, str]]:
|
||||||
"""Check if rule concludes the goal."""
|
"""
|
||||||
return rule.conclusion == goal
|
Try to unify a condition (with vars) against a fact.
|
||||||
|
Returns new bindings if successful, None otherwise.
|
||||||
|
"""
|
||||||
|
# Handle exact string match shortcut
|
||||||
|
if condition == fact:
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
cond_pred, cond_args = self._parse_predicate(condition)
|
||||||
|
fact_pred, fact_args = self._parse_predicate(fact)
|
||||||
|
|
||||||
|
if cond_pred != fact_pred:
|
||||||
|
return None
|
||||||
|
if len(cond_args) != len(fact_args):
|
||||||
|
return None
|
||||||
|
|
||||||
|
new_bindings = bindings.copy()
|
||||||
|
for c_arg, f_arg in zip(cond_args, fact_args):
|
||||||
|
if c_arg.startswith("?"):
|
||||||
|
if c_arg in new_bindings:
|
||||||
|
if new_bindings[c_arg] != f_arg:
|
||||||
|
return None # Conflict
|
||||||
|
else:
|
||||||
|
new_bindings[c_arg] = f_arg
|
||||||
|
else:
|
||||||
|
if c_arg != f_arg:
|
||||||
|
return None # Constant mismatch
|
||||||
|
return new_bindings
|
||||||
|
|
||||||
|
def _find_matches(self, conditions: List[str], bindings: Dict[str, str]) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Recursively find all bindings that satisfy the conditions.
|
||||||
|
"""
|
||||||
|
if not conditions:
|
||||||
|
return [bindings]
|
||||||
|
|
||||||
|
first = conditions[0]
|
||||||
|
# Substitute current bindings into first condition before matching
|
||||||
|
first_substituted = self._substitute_bindings(first, bindings)
|
||||||
|
rest = conditions[1:]
|
||||||
|
|
||||||
|
valid_bindings = []
|
||||||
|
|
||||||
|
# Try to match 'first' against all facts
|
||||||
|
for fact in self.facts:
|
||||||
|
# Skip if fact is not a string (unhashable/objects) for now, or handle str()
|
||||||
|
if not isinstance(fact, str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
unified = self._unify(first_substituted, fact, bindings)
|
||||||
|
if unified is not None:
|
||||||
|
# Recursive step
|
||||||
|
results = self._find_matches(rest, unified)
|
||||||
|
valid_bindings.extend(results)
|
||||||
|
|
||||||
|
return valid_bindings
|
||||||
|
|
||||||
|
def _substitute_bindings(self, text: str, bindings: Dict[str, str]) -> str:
|
||||||
|
"""Substitute variables in text with bindings."""
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return text
|
||||||
|
pred, args = self._parse_predicate(text)
|
||||||
|
if not args:
|
||||||
|
return text
|
||||||
|
|
||||||
|
new_args = []
|
||||||
|
for arg in args:
|
||||||
|
if arg in bindings:
|
||||||
|
new_args.append(bindings[arg])
|
||||||
|
else:
|
||||||
|
new_args.append(arg)
|
||||||
|
|
||||||
|
return f"{pred}({', '.join(new_args)})"
|
||||||
|
|
||||||
def _apply_rule(
|
def _apply_rule(
|
||||||
self, rule: Rule, premises: Optional[List[Any]] = None
|
self, rule: Rule, premises: Optional[List[Any]] = None, bindings: Optional[Dict[str, str]] = None
|
||||||
) -> Optional[InferenceResult]:
|
) -> Optional[InferenceResult]:
|
||||||
"""Apply rule and return inference result."""
|
"""Apply rule and return inference result."""
|
||||||
|
conclusion = rule.conclusion
|
||||||
|
if bindings:
|
||||||
|
conclusion = self._substitute_bindings(conclusion, bindings)
|
||||||
|
|
||||||
if premises is None:
|
if premises is None:
|
||||||
premises = list(rule.conditions)
|
# Reconstruct premises from bindings if not provided (approximate)
|
||||||
|
premises = [self._substitute_bindings(c, bindings or {}) for c in rule.conditions]
|
||||||
|
|
||||||
result = InferenceResult(
|
result = InferenceResult(
|
||||||
conclusion=rule.conclusion,
|
conclusion=conclusion,
|
||||||
premises=premises,
|
premises=premises,
|
||||||
rule_used=rule,
|
rule_used=rule,
|
||||||
confidence=rule.confidence,
|
confidence=rule.confidence,
|
||||||
metadata={"rule_name": rule.name, "rule_id": rule.rule_id},
|
metadata={"rule_name": rule.name, "rule_id": rule.rule_id, "bindings": bindings},
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def infer(self, query: Any, **options) -> List[InferenceResult]:
|
def infer(self, query: Any, **options) -> List[InferenceResult]:
|
||||||
"""
|
"""
|
||||||
Perform inference based on strategy.
|
Perform inference based on strategy.
|
||||||
|
|||||||
@@ -194,18 +194,21 @@ class SeedDataManager:
|
|||||||
entity_type: Optional[str] = None,
|
entity_type: Optional[str] = None,
|
||||||
relationship_type: Optional[str] = None,
|
relationship_type: Optional[str] = None,
|
||||||
source_name: Optional[str] = None,
|
source_name: Optional[str] = None,
|
||||||
|
delimiter: Optional[str] = None,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Load seed data from CSV file.
|
Load seed data from CSV file.
|
||||||
|
|
||||||
Reads a CSV file and converts rows to dictionaries. Automatically
|
Reads a CSV file and converts rows to dictionaries. Automatically
|
||||||
adds entity_type, relationship_type, and source metadata if provided.
|
adds entity_type, relationship_type, and source metadata if provided.
|
||||||
|
Supports automatic delimiter detection if not provided.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Path to CSV file
|
file_path: Path to CSV file
|
||||||
entity_type: Optional entity type to add to all records
|
entity_type: Optional entity type to add to all records
|
||||||
relationship_type: Optional relationship type to add to all records
|
relationship_type: Optional relationship type to add to all records
|
||||||
source_name: Optional source name for tracking
|
source_name: Optional source name for tracking
|
||||||
|
delimiter: Optional CSV delimiter. If None, attempts to detect it.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of loaded data records as dictionaries
|
List of loaded data records as dictionaries
|
||||||
@@ -215,7 +218,7 @@ class SeedDataManager:
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> records = manager.load_from_csv("data/entities.csv", entity_type="Person")
|
>>> records = manager.load_from_csv("data/entities.csv", entity_type="Person")
|
||||||
>>> print(f"Loaded {len(records)} records")
|
>>> records = manager.load_from_csv("data/data.csv", delimiter=";")
|
||||||
"""
|
"""
|
||||||
tracking_id = self.progress_tracker.start_tracking(
|
tracking_id = self.progress_tracker.start_tracking(
|
||||||
module="seed",
|
module="seed",
|
||||||
@@ -239,7 +242,21 @@ class SeedDataManager:
|
|||||||
tracking_id, message="Reading CSV file..."
|
tracking_id, message="Reading CSV file..."
|
||||||
)
|
)
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
reader = csv.DictReader(f)
|
# Detect delimiter if not provided
|
||||||
|
if delimiter is None:
|
||||||
|
try:
|
||||||
|
sample = f.read(1024)
|
||||||
|
f.seek(0)
|
||||||
|
dialect = csv.Sniffer().sniff(sample)
|
||||||
|
delimiter = dialect.delimiter
|
||||||
|
self.logger.debug(f"Detected CSV delimiter: '{delimiter}'")
|
||||||
|
except csv.Error:
|
||||||
|
# Fallback to comma if sniffing fails
|
||||||
|
f.seek(0)
|
||||||
|
delimiter = ","
|
||||||
|
self.logger.debug("Could not detect delimiter, defaulting to ','")
|
||||||
|
|
||||||
|
reader = csv.DictReader(f, delimiter=delimiter)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
# Clean up row data
|
# Clean up row data
|
||||||
record = {k: v for k, v in row.items() if v}
|
record = {k: v for k, v in row.items() if v}
|
||||||
@@ -316,6 +333,11 @@ class SeedDataManager:
|
|||||||
elif "records" in data:
|
elif "records" in data:
|
||||||
records = data["records"]
|
records = data["records"]
|
||||||
else:
|
else:
|
||||||
|
self.logger.warning(
|
||||||
|
f"JSON file {file_path} is a dictionary but contains none of the "
|
||||||
|
"expected keys: 'entities', 'data', 'records'. "
|
||||||
|
"Treating entire object as a single record."
|
||||||
|
)
|
||||||
records = [data]
|
records = [data]
|
||||||
else:
|
else:
|
||||||
records = []
|
records = []
|
||||||
|
|||||||
@@ -48,6 +48,17 @@ records = manager.load_from_csv(
|
|||||||
entity_type="Person"
|
entity_type="Person"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Load from CSV with custom delimiter
|
||||||
|
records_pipe = manager.load_from_csv(
|
||||||
|
"data/entities_pipe.csv",
|
||||||
|
delimiter="|"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load from CSV with auto-detection (supported for common delimiters like ;, \t, etc.)
|
||||||
|
records_auto = manager.load_from_csv(
|
||||||
|
"data/entities_semicolon.csv"
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Loaded {len(records)} records from CSV")
|
print(f"Loaded {len(records)} records from CSV")
|
||||||
|
|
||||||
# CSV should have columns like: id, name, type, etc.
|
# CSV should have columns like: id, name, type, etc.
|
||||||
@@ -74,6 +85,10 @@ print(f"Loaded {len(records)} records from JSON")
|
|||||||
# - List: [{"id": "1", "name": "John"}, ...]
|
# - List: [{"id": "1", "name": "John"}, ...]
|
||||||
# - Dict with 'entities': {"entities": [...]}
|
# - Dict with 'entities': {"entities": [...]}
|
||||||
# - Dict with 'data': {"data": [...]}
|
# - Dict with 'data': {"data": [...]}
|
||||||
|
# - Dict with 'records': {"records": [...]}
|
||||||
|
#
|
||||||
|
# Note: Ensure JSON seed files follow these supported top-level structures.
|
||||||
|
# Unsupported structures will trigger a warning and may be loaded as a single record.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Loading from Database
|
### Loading from Database
|
||||||
@@ -553,11 +568,15 @@ manager.export_seed_data("output/custom_seed.json", format="json")
|
|||||||
**Algorithm**: Row-by-row CSV processing with metadata injection
|
**Algorithm**: Row-by-row CSV processing with metadata injection
|
||||||
|
|
||||||
1. **File Reading**: Open CSV file with UTF-8 encoding
|
1. **File Reading**: Open CSV file with UTF-8 encoding
|
||||||
2. **Header Detection**: Use csv.DictReader() for automatic header detection
|
2. **Delimiter Detection**:
|
||||||
3. **Row Processing**: Iterate through rows, convert to dictionaries
|
- Use provided delimiter if specified
|
||||||
4. **Data Cleaning**: Remove empty values, clean whitespace
|
- If not, attempt to auto-detect delimiter using `csv.Sniffer`
|
||||||
5. **Metadata Injection**: Add entity_type, relationship_type, source metadata
|
- Fallback to comma (`,`) if detection fails
|
||||||
6. **Type Conversion**: Convert string values to appropriate types
|
3. **Header Detection**: Use csv.DictReader() for automatic header detection
|
||||||
|
4. **Row Processing**: Iterate through rows, convert to dictionaries
|
||||||
|
5. **Data Cleaning**: Remove empty values, clean whitespace
|
||||||
|
6. **Metadata Injection**: Add entity_type, relationship_type, source metadata
|
||||||
|
7. **Type Conversion**: Convert string values to appropriate types
|
||||||
|
|
||||||
**Time Complexity**: O(n) where n = number of rows
|
**Time Complexity**: O(n) where n = number of rows
|
||||||
**Space Complexity**: O(n) for records storage
|
**Space Complexity**: O(n) for records storage
|
||||||
|
|||||||
@@ -311,6 +311,10 @@ def extract_entities_llm(
|
|||||||
text: str, provider: str = "openai", model: Optional[str] = None, **kwargs
|
text: str, provider: str = "openai", model: Optional[str] = None, **kwargs
|
||||||
) -> List[Entity]:
|
) -> List[Entity]:
|
||||||
"""LLM-based entity extraction."""
|
"""LLM-based entity extraction."""
|
||||||
|
# Support llm_model parameter to disambiguate from ML model
|
||||||
|
if "llm_model" in kwargs:
|
||||||
|
model = kwargs.pop("llm_model")
|
||||||
|
|
||||||
llm = create_provider(provider, model=model, **kwargs)
|
llm = create_provider(provider, model=model, **kwargs)
|
||||||
|
|
||||||
if not llm.is_available():
|
if not llm.is_available():
|
||||||
@@ -818,6 +822,7 @@ def get_entity_method(method_name: str):
|
|||||||
"regex": extract_entities_regex,
|
"regex": extract_entities_regex,
|
||||||
"rules": extract_entities_rules,
|
"rules": extract_entities_rules,
|
||||||
"ml": extract_entities_ml,
|
"ml": extract_entities_ml,
|
||||||
|
"spacy": extract_entities_ml, # Alias for ml
|
||||||
"huggingface": extract_entities_huggingface,
|
"huggingface": extract_entities_huggingface,
|
||||||
"llm": extract_entities_llm,
|
"llm": extract_entities_llm,
|
||||||
}
|
}
|
||||||
@@ -844,6 +849,8 @@ def get_relation_method(method_name: str):
|
|||||||
"regex": extract_relations_regex,
|
"regex": extract_relations_regex,
|
||||||
"cooccurrence": extract_relations_cooccurrence,
|
"cooccurrence": extract_relations_cooccurrence,
|
||||||
"dependency": extract_relations_dependency,
|
"dependency": extract_relations_dependency,
|
||||||
|
"ml": extract_relations_dependency, # Alias for dependency
|
||||||
|
"spacy": extract_relations_dependency, # Alias for dependency
|
||||||
"huggingface": extract_relations_huggingface,
|
"huggingface": extract_relations_huggingface,
|
||||||
"llm": extract_relations_llm,
|
"llm": extract_relations_llm,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class NamedEntityRecognizer:
|
|||||||
# Use NERExtractor for actual extraction
|
# Use NERExtractor for actual extraction
|
||||||
ner_config = self.config.get("ner", {})
|
ner_config = self.config.get("ner", {})
|
||||||
ner_config["confidence_threshold"] = confidence_threshold
|
ner_config["confidence_threshold"] = confidence_threshold
|
||||||
|
ner_config["min_confidence"] = confidence_threshold
|
||||||
ner_config["merge_overlapping"] = merge_overlapping
|
ner_config["merge_overlapping"] = merge_overlapping
|
||||||
if method is not None:
|
if method is not None:
|
||||||
ner_config["method"] = method
|
ner_config["method"] = method
|
||||||
|
|||||||
@@ -142,6 +142,19 @@ class NERExtractor:
|
|||||||
f"spaCy model {self.model_name} not found. ML method will fallback."
|
f"spaCy model {self.model_name} not found. ML method will fallback."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def extract(self, text: str, **kwargs) -> List[Entity]:
|
||||||
|
"""
|
||||||
|
Alias for extract_entities.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Input text
|
||||||
|
**kwargs: Extraction options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of extracted entities
|
||||||
|
"""
|
||||||
|
return self.extract_entities(text, **kwargs)
|
||||||
|
|
||||||
def extract_entities(self, text: str, **options) -> List[Entity]:
|
def extract_entities(self, text: str, **options) -> List[Entity]:
|
||||||
"""
|
"""
|
||||||
Extract named entities from text.
|
Extract named entities from text.
|
||||||
|
|||||||
@@ -155,6 +155,20 @@ class RelationExtractor:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract(self, text: str, entities: List[Entity], **kwargs) -> List[Relation]:
|
||||||
|
"""
|
||||||
|
Alias for extract_relations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Input text
|
||||||
|
entities: List of entities in the text
|
||||||
|
**kwargs: Extraction options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of extracted relations
|
||||||
|
"""
|
||||||
|
return self.extract_relations(text, entities, **kwargs)
|
||||||
|
|
||||||
def extract_relations(
|
def extract_relations(
|
||||||
self, text: str, entities: List[Entity], **options
|
self, text: str, entities: List[Entity], **options
|
||||||
) -> List[Relation]:
|
) -> List[Relation]:
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ This comprehensive guide demonstrates how to use the semantic extraction module
|
|||||||
6. [Coreference Resolution](#coreference-resolution)
|
6. [Coreference Resolution](#coreference-resolution)
|
||||||
7. [Semantic Analysis](#semantic-analysis)
|
7. [Semantic Analysis](#semantic-analysis)
|
||||||
8. [Semantic Networks](#semantic-networks)
|
8. [Semantic Networks](#semantic-networks)
|
||||||
9. [Using Methods](#using-methods)
|
9. [Using Registry](#using-registry)
|
||||||
10. [Using Registry](#using-registry)
|
10. [Configuration](#configuration)
|
||||||
11. [Configuration](#configuration)
|
11. [Advanced Examples](#advanced-examples)
|
||||||
12. [Advanced Examples](#advanced-examples)
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ print(f"Entities: {entities}")
|
|||||||
|
|
||||||
# Extract relations
|
# Extract relations
|
||||||
rel_extractor = RelationExtractor()
|
rel_extractor = RelationExtractor()
|
||||||
relations = rel_extractor.extract_relations(text, entities=entities)
|
relations = rel_extractor.extract(text, entities=entities)
|
||||||
print(f"Relations: {relations}")
|
print(f"Relations: {relations}")
|
||||||
|
|
||||||
print(f"Extracted {len(entities)} entities and {len(relations)} relations")
|
print(f"Extracted {len(entities)} entities and {len(relations)} relations")
|
||||||
@@ -55,33 +54,33 @@ for entity in entities:
|
|||||||
### Different Entity Extraction Methods
|
### Different Entity Extraction Methods
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from semantica.semantic_extract.methods import get_entity_method
|
from semantica.semantic_extract import NERExtractor
|
||||||
|
|
||||||
text = "Apple Inc. was founded by Steve Jobs in 1976."
|
text = "Apple Inc. was founded by Steve Jobs in 1976."
|
||||||
|
|
||||||
# Pattern-based extraction
|
# Pattern-based extraction
|
||||||
pattern_method = get_entity_method("pattern")
|
extractor = NERExtractor(method="pattern")
|
||||||
entities = pattern_method(text)
|
entities = extractor.extract(text)
|
||||||
print(f"Pattern method: {len(entities)} entities")
|
print(f"Pattern method: {len(entities)} entities")
|
||||||
|
|
||||||
# Regex-based extraction
|
# Regex-based extraction
|
||||||
regex_method = get_entity_method("regex")
|
extractor = NERExtractor(method="regex")
|
||||||
entities = regex_method(text)
|
entities = extractor.extract(text)
|
||||||
print(f"Regex method: {len(entities)} entities")
|
print(f"Regex method: {len(entities)} entities")
|
||||||
|
|
||||||
# ML-based extraction (spaCy)
|
# ML-based extraction (spaCy)
|
||||||
ml_method = get_entity_method("ml")
|
extractor = NERExtractor(method="ml")
|
||||||
entities = ml_method(text)
|
entities = extractor.extract(text)
|
||||||
print(f"ML method: {len(entities)} entities")
|
print(f"ML method: {len(entities)} entities")
|
||||||
|
|
||||||
# HuggingFace model extraction
|
# HuggingFace model extraction
|
||||||
hf_method = get_entity_method("huggingface")
|
extractor = NERExtractor(method="huggingface")
|
||||||
entities = hf_method(text, model="dslim/bert-base-NER")
|
entities = extractor.extract(text, model="dslim/bert-base-NER")
|
||||||
print(f"HuggingFace method: {len(entities)} entities")
|
print(f"HuggingFace method: {len(entities)} entities")
|
||||||
|
|
||||||
# LLM-based extraction
|
# LLM-based extraction
|
||||||
llm_method = get_entity_method("llm")
|
extractor = NERExtractor(method="llm")
|
||||||
entities = llm_method(text, provider="openai", model="gpt-4")
|
entities = extractor.extract(text, provider="openai", model="gpt-4")
|
||||||
print(f"LLM method: {len(entities)} entities")
|
print(f"LLM method: {len(entities)} entities")
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -90,13 +89,29 @@ print(f"LLM method: {len(entities)} entities")
|
|||||||
```python
|
```python
|
||||||
from semantica.semantic_extract import NERExtractor
|
from semantica.semantic_extract import NERExtractor
|
||||||
|
|
||||||
extractor = NERExtractor(method="ml")
|
# 1. Standard ML (spaCy)
|
||||||
|
extractor = NERExtractor(method="ml", model="en_core_web_trf")
|
||||||
|
entities = extractor.extract(text)
|
||||||
|
|
||||||
|
# 2. LLM-based extraction
|
||||||
|
extractor = NERExtractor(
|
||||||
|
method="llm",
|
||||||
|
provider="openai",
|
||||||
|
model="gpt-4",
|
||||||
|
temperature=0.0
|
||||||
|
)
|
||||||
|
entities = extractor.extract(text)
|
||||||
|
|
||||||
|
# 3. Regex with custom patterns
|
||||||
|
extractor = NERExtractor(
|
||||||
|
method="regex",
|
||||||
|
patterns={"CODE": r"[A-Z]{3}-\d{3}"}
|
||||||
|
)
|
||||||
entities = extractor.extract(text)
|
entities = extractor.extract(text)
|
||||||
|
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
print(f"Entity: {entity.text}")
|
print(f"Entity: {entity.text}")
|
||||||
print(f" Type: {entity.type}")
|
print(f" Type: {entity.label}")
|
||||||
print(f" Start: {entity.start}, End: {entity.end}")
|
|
||||||
print(f" Confidence: {entity.confidence}")
|
print(f" Confidence: {entity.confidence}")
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -129,7 +144,7 @@ from semantica.semantic_extract import RelationExtractor
|
|||||||
extractor = RelationExtractor()
|
extractor = RelationExtractor()
|
||||||
text = "Steve Jobs founded Apple Inc. in 1976."
|
text = "Steve Jobs founded Apple Inc. in 1976."
|
||||||
|
|
||||||
relations = extractor.extract_relations(text, entities=entities)
|
relations = extractor.extract(text, entities=entities)
|
||||||
|
|
||||||
for relation in relations:
|
for relation in relations:
|
||||||
print(f"{relation.subject} --[{relation.predicate}]--> {relation.object}")
|
print(f"{relation.subject} --[{relation.predicate}]--> {relation.object}")
|
||||||
@@ -139,29 +154,29 @@ for relation in relations:
|
|||||||
### Different Relation Extraction Methods
|
### Different Relation Extraction Methods
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from semantica.semantic_extract.methods import get_relation_method
|
from semantica.semantic_extract import RelationExtractor
|
||||||
|
|
||||||
text = "Steve Jobs founded Apple Inc."
|
text = "Steve Jobs founded Apple Inc."
|
||||||
|
|
||||||
# Pattern-based extraction
|
# Pattern-based extraction
|
||||||
pattern_method = get_relation_method("pattern")
|
extractor = RelationExtractor(method="pattern")
|
||||||
relations = pattern_method(text, entities=entities)
|
relations = extractor.extract(text, entities=entities)
|
||||||
|
|
||||||
# Dependency parsing-based
|
# Dependency parsing-based
|
||||||
dependency_method = get_relation_method("dependency")
|
extractor = RelationExtractor(method="dependency")
|
||||||
relations = dependency_method(text, entities=entities)
|
relations = extractor.extract(text, entities=entities)
|
||||||
|
|
||||||
# Co-occurrence based
|
# Co-occurrence based
|
||||||
cooccurrence_method = get_relation_method("cooccurrence")
|
extractor = RelationExtractor(method="cooccurrence")
|
||||||
relations = cooccurrence_method(text, entities=entities)
|
relations = extractor.extract(text, entities=entities)
|
||||||
|
|
||||||
# HuggingFace model
|
# HuggingFace model
|
||||||
hf_method = get_relation_method("huggingface")
|
extractor = RelationExtractor(method="huggingface")
|
||||||
relations = hf_method(text, entities=entities, model="microsoft/DialoGPT-medium")
|
relations = extractor.extract(text, entities=entities, model="microsoft/DialoGPT-medium")
|
||||||
|
|
||||||
# LLM-based
|
# LLM-based
|
||||||
llm_method = get_relation_method("llm")
|
extractor = RelationExtractor(method="llm")
|
||||||
relations = llm_method(text, entities=entities, provider="openai")
|
relations = extractor.extract(text, entities=entities, provider="openai")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Relation Types
|
### Relation Types
|
||||||
@@ -201,25 +216,25 @@ for triple in triples:
|
|||||||
### Different Triple Extraction Methods
|
### Different Triple Extraction Methods
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from semantica.semantic_extract.methods import get_triple_method
|
from semantica.semantic_extract import TripleExtractor
|
||||||
|
|
||||||
text = "Apple Inc. was founded by Steve Jobs in 1976."
|
text = "Apple Inc. was founded by Steve Jobs in 1976."
|
||||||
|
|
||||||
# Pattern-based
|
# Pattern-based
|
||||||
pattern_method = get_triple_method("pattern")
|
extractor = TripleExtractor(method="pattern")
|
||||||
triples = pattern_method(text)
|
triples = extractor.extract_triples(text)
|
||||||
|
|
||||||
# Rules-based
|
# Rules-based
|
||||||
rules_method = get_triple_method("rules")
|
extractor = TripleExtractor(method="rules")
|
||||||
triples = rules_method(text)
|
triples = extractor.extract_triples(text)
|
||||||
|
|
||||||
# HuggingFace model
|
# HuggingFace model
|
||||||
hf_method = get_triple_method("huggingface")
|
extractor = TripleExtractor(method="huggingface")
|
||||||
triples = hf_method(text, model="t5-base")
|
triples = extractor.extract_triples(text, model="t5-base")
|
||||||
|
|
||||||
# LLM-based
|
# LLM-based
|
||||||
llm_method = get_triple_method("llm")
|
extractor = TripleExtractor(method="llm")
|
||||||
triples = llm_method(text, provider="openai", model="gpt-4")
|
triples = extractor.extract_triples(text, provider="openai", model="gpt-4")
|
||||||
```
|
```
|
||||||
|
|
||||||
### RDF Serialization
|
### RDF Serialization
|
||||||
@@ -462,29 +477,6 @@ print(f"Node: {node.label}")
|
|||||||
print(f"Edge: {edge.source} --[{edge.relation}]--> {edge.target}")
|
print(f"Edge: {edge.source} --[{edge.relation}]--> {edge.target}")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Methods
|
|
||||||
|
|
||||||
### Getting Available Methods
|
|
||||||
|
|
||||||
```python
|
|
||||||
from semantica.semantic_extract.methods import (
|
|
||||||
get_entity_method,
|
|
||||||
get_relation_method,
|
|
||||||
get_triple_method
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get entity extraction method
|
|
||||||
entity_method = get_entity_method("llm")
|
|
||||||
entities = entity_method(text, provider="openai")
|
|
||||||
|
|
||||||
# Get relation extraction method
|
|
||||||
relation_method = get_relation_method("dependency")
|
|
||||||
relations = relation_method(text, entities=entities)
|
|
||||||
|
|
||||||
# Get triple extraction method
|
|
||||||
triple_method = get_triple_method("pattern")
|
|
||||||
triples = triple_method(text)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using Registry
|
## Using Registry
|
||||||
|
|
||||||
@@ -504,9 +496,9 @@ def custom_entity_extraction(text, **kwargs):
|
|||||||
method_registry.register("entity", "custom_method", custom_entity_extraction)
|
method_registry.register("entity", "custom_method", custom_entity_extraction)
|
||||||
|
|
||||||
# Use custom method
|
# Use custom method
|
||||||
from semantica.semantic_extract.methods import get_entity_method
|
from semantica.semantic_extract import NERExtractor
|
||||||
custom_method = get_entity_method("custom_method")
|
extractor = NERExtractor(method="custom_method")
|
||||||
entities = custom_method(text)
|
entities = extractor.extract(text)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Listing Registered Methods
|
### Listing Registered Methods
|
||||||
|
|||||||
+115
-3
@@ -160,6 +160,15 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
SEMANTIC_EXTRACT_AVAILABLE = False
|
SEMANTIC_EXTRACT_AVAILABLE = False
|
||||||
|
|
||||||
|
# Import specialized chunkers
|
||||||
|
try:
|
||||||
|
from .structural_chunker import StructuralChunker
|
||||||
|
from .sliding_window_chunker import SlidingWindowChunker
|
||||||
|
|
||||||
|
SPECIALIZED_CHUNKERS_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
SPECIALIZED_CHUNKERS_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Standard Splitting Methods
|
# Standard Splitting Methods
|
||||||
@@ -1012,9 +1021,14 @@ def split_relation_aware(
|
|||||||
return split_recursive(text, chunk_size=chunk_size, **kwargs)
|
return split_recursive(text, chunk_size=chunk_size, **kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Extract entities first (required for relation extraction)
|
||||||
|
ner_method = kwargs.get("ner_method", "ml")
|
||||||
|
ner_extractor = NERExtractor(method=ner_method, **kwargs)
|
||||||
|
entities = ner_extractor.extract(text)
|
||||||
|
|
||||||
# Extract relations/triples
|
# Extract relations/triples
|
||||||
relation_extractor = RelationExtractor(method=relation_method, **kwargs)
|
relation_extractor = RelationExtractor(method=relation_method, **kwargs)
|
||||||
relations = relation_extractor.extract(text)
|
relations = relation_extractor.extract(text, entities)
|
||||||
|
|
||||||
# Create triple boundaries (subject, relation, object must be in same chunk)
|
# Create triple boundaries (subject, relation, object must be in same chunk)
|
||||||
triple_boundaries = []
|
triple_boundaries = []
|
||||||
@@ -1412,13 +1426,23 @@ def split_hierarchical(
|
|||||||
|
|
||||||
# Fall back to paragraph level
|
# Fall back to paragraph level
|
||||||
if "paragraph" in levels:
|
if "paragraph" in levels:
|
||||||
|
# Remove chunk_size from kwargs to avoid multiple values error
|
||||||
|
para_kwargs = kwargs.copy()
|
||||||
|
if "chunk_size" in para_kwargs:
|
||||||
|
del para_kwargs["chunk_size"]
|
||||||
|
|
||||||
return split_by_paragraphs(
|
return split_by_paragraphs(
|
||||||
text, chunk_size=chunk_sizes[0] if chunk_sizes else 1000, **kwargs
|
text, chunk_size=chunk_sizes[0] if chunk_sizes else 1000, **para_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fall back to sentence level
|
# Fall back to sentence level
|
||||||
|
# Remove chunk_size from kwargs to avoid multiple values error
|
||||||
|
sent_kwargs = kwargs.copy()
|
||||||
|
if "chunk_size" in sent_kwargs:
|
||||||
|
del sent_kwargs["chunk_size"]
|
||||||
|
|
||||||
return split_by_sentences(
|
return split_by_sentences(
|
||||||
text, chunk_size=chunk_sizes[0] if chunk_sizes else 1000, **kwargs
|
text, chunk_size=chunk_sizes[0] if chunk_sizes else 1000, **sent_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1515,6 +1539,91 @@ def split_topic_based(
|
|||||||
return split_semantic_transformer(text, chunk_size=chunk_size, **kwargs)
|
return split_semantic_transformer(text, chunk_size=chunk_size, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def split_structural(
|
||||||
|
text: str,
|
||||||
|
max_chunk_size: int = 2000,
|
||||||
|
respect_headers: bool = True,
|
||||||
|
respect_sections: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
) -> List[Chunk]:
|
||||||
|
"""
|
||||||
|
Structure-aware chunking respecting document hierarchy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Input text
|
||||||
|
max_chunk_size: Maximum chunk size
|
||||||
|
respect_headers: Whether to respect heading hierarchy
|
||||||
|
respect_sections: Whether to respect section boundaries
|
||||||
|
**kwargs: Additional options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of chunks
|
||||||
|
"""
|
||||||
|
if not SPECIALIZED_CHUNKERS_AVAILABLE:
|
||||||
|
logger.warning(
|
||||||
|
"StructuralChunker not available, falling back to recursive splitting"
|
||||||
|
)
|
||||||
|
return split_recursive(text, chunk_size=max_chunk_size, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
chunker = StructuralChunker(
|
||||||
|
max_chunk_size=max_chunk_size,
|
||||||
|
respect_headers=respect_headers,
|
||||||
|
respect_sections=respect_sections,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return chunker.chunk(text, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Error in structural splitting: {e}, falling back to recursive splitting"
|
||||||
|
)
|
||||||
|
return split_recursive(text, chunk_size=max_chunk_size, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def split_sliding_window(
|
||||||
|
text: str,
|
||||||
|
chunk_size: int = 1000,
|
||||||
|
overlap: int = 200,
|
||||||
|
stride: Optional[int] = None,
|
||||||
|
preserve_boundaries: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
) -> List[Chunk]:
|
||||||
|
"""
|
||||||
|
Sliding window chunking with optional boundary preservation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Input text
|
||||||
|
chunk_size: Chunk size in characters
|
||||||
|
overlap: Overlap size in characters
|
||||||
|
stride: Stride size (default: chunk_size - overlap)
|
||||||
|
preserve_boundaries: Whether to preserve word/sentence boundaries
|
||||||
|
**kwargs: Additional options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of chunks
|
||||||
|
"""
|
||||||
|
if not SPECIALIZED_CHUNKERS_AVAILABLE:
|
||||||
|
logger.warning(
|
||||||
|
"SlidingWindowChunker not available, falling back to recursive splitting"
|
||||||
|
)
|
||||||
|
return split_recursive(
|
||||||
|
text, chunk_size=chunk_size, chunk_overlap=overlap, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
chunker = SlidingWindowChunker(
|
||||||
|
chunk_size=chunk_size, overlap=overlap, stride=stride, **kwargs
|
||||||
|
)
|
||||||
|
return chunker.chunk(text, preserve_boundaries=preserve_boundaries, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Error in sliding window splitting: {e}, falling back to recursive splitting"
|
||||||
|
)
|
||||||
|
return split_recursive(
|
||||||
|
text, chunk_size=chunk_size, chunk_overlap=overlap, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Method Dispatcher
|
# Method Dispatcher
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -1542,6 +1651,9 @@ _SPLIT_METHODS = {
|
|||||||
"centrality_based": split_centrality_based,
|
"centrality_based": split_centrality_based,
|
||||||
"subgraph": split_subgraph,
|
"subgraph": split_subgraph,
|
||||||
"topic_based": split_topic_based,
|
"topic_based": split_topic_based,
|
||||||
|
# Specialized methods
|
||||||
|
"structural": split_structural,
|
||||||
|
"sliding_window": split_sliding_window,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ chunks = split_entity_aware(
|
|||||||
text,
|
text,
|
||||||
chunk_size=1000,
|
chunk_size=1000,
|
||||||
chunk_overlap=200,
|
chunk_overlap=200,
|
||||||
ner_method="llm", # or "spacy", "huggingface"
|
ner_method="ml", # "ml" (spaCy), "llm", or "pattern"
|
||||||
preserve_entities=True
|
preserve_entities=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -324,7 +324,7 @@ chunks = table_chunker.chunk(text_with_tables)
|
|||||||
entity_chunker = EntityAwareChunker(
|
entity_chunker = EntityAwareChunker(
|
||||||
chunk_size=1000,
|
chunk_size=1000,
|
||||||
chunk_overlap=200,
|
chunk_overlap=200,
|
||||||
ner_method="llm",
|
ner_method="ml",
|
||||||
preserve_entities=True
|
preserve_entities=True
|
||||||
)
|
)
|
||||||
chunks = entity_chunker.chunk(text)
|
chunks = entity_chunker.chunk(text)
|
||||||
@@ -408,7 +408,7 @@ chunks6 = split_by_words(text, chunk_size=500, chunk_overlap=50)
|
|||||||
# Advanced methods
|
# Advanced methods
|
||||||
chunks7 = split_semantic_transformer(text, chunk_size=1000, chunk_overlap=200)
|
chunks7 = split_semantic_transformer(text, chunk_size=1000, chunk_overlap=200)
|
||||||
chunks8 = split_llm(text, chunk_size=1000, provider="openai")
|
chunks8 = split_llm(text, chunk_size=1000, provider="openai")
|
||||||
chunks9 = split_entity_aware(text, chunk_size=1000, ner_method="llm")
|
chunks9 = split_entity_aware(text, chunk_size=1000, ner_method="ml")
|
||||||
chunks10 = split_relation_aware(text, chunk_size=1000)
|
chunks10 = split_relation_aware(text, chunk_size=1000)
|
||||||
chunks11 = split_graph_based(text, chunk_size=1000)
|
chunks11 = split_graph_based(text, chunk_size=1000)
|
||||||
chunks12 = split_ontology_aware(text, chunk_size=1000)
|
chunks12 = split_ontology_aware(text, chunk_size=1000)
|
||||||
|
|||||||
@@ -302,14 +302,22 @@ class OntologyVisualizer:
|
|||||||
# Add domain edges
|
# Add domain edges
|
||||||
domain = prop.get("domain")
|
domain = prop.get("domain")
|
||||||
if domain:
|
if domain:
|
||||||
edges.append({"source": prop_name, "target": domain, "type": "domain"})
|
if isinstance(domain, list):
|
||||||
|
for d in domain:
|
||||||
|
edges.append({"source": prop_name, "target": d, "type": "domain"})
|
||||||
|
else:
|
||||||
|
edges.append({"source": prop_name, "target": domain, "type": "domain"})
|
||||||
|
|
||||||
# Add range edges
|
# Add range edges
|
||||||
range_val = prop.get("range")
|
range_val = prop.get("range")
|
||||||
if range_val:
|
if range_val:
|
||||||
edges.append(
|
if isinstance(range_val, list):
|
||||||
{"source": prop_name, "target": range_val, "type": "range"}
|
for r in range_val:
|
||||||
)
|
edges.append({"source": prop_name, "target": r, "type": "range"})
|
||||||
|
else:
|
||||||
|
edges.append(
|
||||||
|
{"source": prop_name, "target": range_val, "type": "range"}
|
||||||
|
)
|
||||||
|
|
||||||
return self._visualize_structure_plotly(
|
return self._visualize_structure_plotly(
|
||||||
nodes, edges, output, file_path, **options
|
nodes, edges, output, file_path, **options
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,209 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from semantica.ontology import (
|
||||||
|
OntologyEngine,
|
||||||
|
ClassInferrer,
|
||||||
|
PropertyGenerator,
|
||||||
|
OntologyOptimizer,
|
||||||
|
OntologyValidator,
|
||||||
|
CompetencyQuestionsManager,
|
||||||
|
LLMOntologyGenerator
|
||||||
|
)
|
||||||
|
from semantica.visualization import OntologyVisualizer
|
||||||
|
|
||||||
|
class TestNotebook14(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests mirroring the steps in cookbook/introduction/14_Ontology.ipynb
|
||||||
|
to ensure the documented examples work correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _run_full_pipeline(self):
|
||||||
|
"""Helper to run the full pipeline and return the ontology."""
|
||||||
|
engine = OntologyEngine(base_uri="https://docs.semantica.dev/ontology/")
|
||||||
|
|
||||||
|
# Sample Data
|
||||||
|
entities = [
|
||||||
|
{"id": "e1", "type": "Company", "name": "TechCorp", "founded": "2010"},
|
||||||
|
{"id": "e2", "type": "Person", "name": "Alice", "role": "CEO"},
|
||||||
|
{"id": "e3", "type": "Person", "name": "Bob", "role": "CTO"},
|
||||||
|
{"id": "e4", "type": "Department", "name": "Engineering"},
|
||||||
|
{"id": "e5", "type": "Project", "name": "Project Phoenix"}
|
||||||
|
]
|
||||||
|
|
||||||
|
relationships = [
|
||||||
|
{"source": "e2", "target": "e1", "type": "leads"},
|
||||||
|
{"source": "e3", "target": "e4", "type": "manages"},
|
||||||
|
{"source": "e4", "target": "e1", "type": "part_of"},
|
||||||
|
{"source": "e3", "target": "e5", "type": "works_on"}
|
||||||
|
]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"entities": entities,
|
||||||
|
"relationships": relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the full pipeline
|
||||||
|
ontology = engine.from_data(data, name="CorporateOntology", min_occurrences=1)
|
||||||
|
return ontology
|
||||||
|
|
||||||
|
def test_full_pipeline(self):
|
||||||
|
"""Test the 6-stage generation pipeline with sample data."""
|
||||||
|
ontology = self._run_full_pipeline()
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
self.assertEqual(ontology['name'], "CorporateOntology")
|
||||||
|
self.assertGreater(len(ontology['classes']), 0)
|
||||||
|
self.assertGreater(len(ontology['properties']), 0)
|
||||||
|
|
||||||
|
# Inspect Classes (just to ensure no errors in access)
|
||||||
|
for cls in ontology['classes']:
|
||||||
|
self.assertIn('name', cls)
|
||||||
|
self.assertIn('uri', cls)
|
||||||
|
|
||||||
|
# Inspect Properties
|
||||||
|
for prop in ontology['properties']:
|
||||||
|
self.assertIn('name', prop)
|
||||||
|
self.assertIn('type', prop)
|
||||||
|
|
||||||
|
def _run_class_inferrer(self):
|
||||||
|
"""Helper to run class inference and return classes."""
|
||||||
|
inferrer = ClassInferrer(min_occurrences=1)
|
||||||
|
|
||||||
|
raw_entities = [
|
||||||
|
{"type": "Manager", "name": "Dave", "level": 5},
|
||||||
|
{"type": "Manager", "name": "Eve", "level": 4},
|
||||||
|
{"type": "Employee", "name": "Frank"},
|
||||||
|
{"type": "TemporaryWorker", "name": "Grace"}
|
||||||
|
]
|
||||||
|
|
||||||
|
classes = inferrer.infer_classes(raw_entities, build_hierarchy=True)
|
||||||
|
return classes
|
||||||
|
|
||||||
|
def test_class_inferrer(self):
|
||||||
|
"""Test ClassInferrer usage."""
|
||||||
|
classes = self._run_class_inferrer()
|
||||||
|
|
||||||
|
self.assertGreater(len(classes), 0)
|
||||||
|
class_names = [c['name'] for c in classes]
|
||||||
|
self.assertIn("Manager", class_names)
|
||||||
|
self.assertIn("Employee", class_names)
|
||||||
|
|
||||||
|
def test_property_generator(self):
|
||||||
|
"""Test PropertyGenerator usage."""
|
||||||
|
# Setup context classes (reusing logic from previous test)
|
||||||
|
classes = self._run_class_inferrer()
|
||||||
|
|
||||||
|
prop_gen = PropertyGenerator()
|
||||||
|
|
||||||
|
complex_entities = [
|
||||||
|
{"id": "m1", "type": "Manager", "name": "Dave", "level": 5},
|
||||||
|
{"id": "e1", "type": "Employee", "name": "Frank"}
|
||||||
|
]
|
||||||
|
complex_relationships = [
|
||||||
|
{"source": "m1", "target": "e1", "type": "supervises"}
|
||||||
|
]
|
||||||
|
|
||||||
|
properties = prop_gen.infer_properties(
|
||||||
|
entities=complex_entities,
|
||||||
|
relationships=complex_relationships,
|
||||||
|
classes=classes,
|
||||||
|
min_occurrences=1
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertGreater(len(properties), 0)
|
||||||
|
prop_names = [p['name'] for p in properties]
|
||||||
|
# "level" should be a data property, "supervises" an object property
|
||||||
|
self.assertTrue(any("level" in p['name'].lower() for p in properties))
|
||||||
|
self.assertTrue(any("supervises" in p['name'].lower() for p in properties))
|
||||||
|
|
||||||
|
def test_ontology_optimizer(self):
|
||||||
|
"""Test OntologyOptimizer usage."""
|
||||||
|
optimizer = OntologyOptimizer()
|
||||||
|
|
||||||
|
messy_ontology = {
|
||||||
|
"classes": [
|
||||||
|
{"name": "Person", "uri": "http://example.org/Person"},
|
||||||
|
{"name": "Person", "uri": "http://example.org/Person"} # Duplicate!
|
||||||
|
],
|
||||||
|
"properties": []
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_ontology = optimizer.optimize_ontology(messy_ontology, remove_redundancy=True)
|
||||||
|
|
||||||
|
self.assertEqual(len(messy_ontology['classes']), 2)
|
||||||
|
self.assertEqual(len(clean_ontology['classes']), 1)
|
||||||
|
|
||||||
|
def test_ontology_validator(self):
|
||||||
|
"""Test OntologyValidator usage."""
|
||||||
|
validator = OntologyValidator(
|
||||||
|
check_consistency=False, # Skip reasoner for unit test speed/dependency
|
||||||
|
check_satisfiability=False
|
||||||
|
)
|
||||||
|
|
||||||
|
ontology = self._run_full_pipeline()
|
||||||
|
result = validator.validate_ontology(ontology)
|
||||||
|
|
||||||
|
self.assertTrue(result.valid)
|
||||||
|
# consistent might be None if check skipped, or True/False.
|
||||||
|
# Just check it runs without error.
|
||||||
|
|
||||||
|
@patch("semantica.visualization.ontology_visualizer.make_subplots")
|
||||||
|
@patch("semantica.visualization.ontology_visualizer.go")
|
||||||
|
def test_visualization(self, mock_go, mock_make_subplots):
|
||||||
|
"""Test OntologyVisualizer usage (mocking plotly)."""
|
||||||
|
viz = OntologyVisualizer()
|
||||||
|
ontology = self._run_full_pipeline()
|
||||||
|
|
||||||
|
# Mock figures
|
||||||
|
mock_fig = MagicMock()
|
||||||
|
mock_go.Figure.return_value = mock_fig
|
||||||
|
mock_make_subplots.return_value = mock_fig
|
||||||
|
mock_go.Scatter.return_value = MagicMock()
|
||||||
|
mock_go.Indicator.return_value = MagicMock()
|
||||||
|
|
||||||
|
# 1. Interactive Class Hierarchy
|
||||||
|
fig_hierarchy = viz.visualize_hierarchy(ontology, output="interactive")
|
||||||
|
# Just check it didn't crash; real test would check calls
|
||||||
|
|
||||||
|
# 2. Ontology Structure Network
|
||||||
|
fig_structure = viz.visualize_structure(ontology, output="interactive")
|
||||||
|
|
||||||
|
# 3. Metrics Dashboard
|
||||||
|
fig_metrics = viz.visualize_metrics(ontology, output="interactive")
|
||||||
|
|
||||||
|
@patch("semantica.ontology.llm_generator.LLMOntologyGenerator.generate_ontology_from_text")
|
||||||
|
def test_llm_ontology_generator(self, mock_generate):
|
||||||
|
"""Test LLMOntologyGenerator (mocked)."""
|
||||||
|
mock_generate.return_value = {
|
||||||
|
"classes": [{"name": "Department"}, {"name": "Course"}],
|
||||||
|
"properties": [],
|
||||||
|
"name": "UniversityOntology"
|
||||||
|
}
|
||||||
|
|
||||||
|
llm_gen = LLMOntologyGenerator(provider="openai", model="gpt-4")
|
||||||
|
|
||||||
|
text_description = "A University has many Departments."
|
||||||
|
|
||||||
|
llm_ontology = llm_gen.generate_ontology_from_text(
|
||||||
|
text=text_description,
|
||||||
|
name="UniversityOntology"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(llm_ontology['name'], "UniversityOntology")
|
||||||
|
self.assertEqual(len(llm_ontology['classes']), 2)
|
||||||
|
|
||||||
|
def test_competency_questions(self):
|
||||||
|
"""Test CompetencyQuestionsManager."""
|
||||||
|
cq_manager = CompetencyQuestionsManager()
|
||||||
|
|
||||||
|
cq_manager.add_question("Who is the CEO?", category="general")
|
||||||
|
questions = cq_manager.questions
|
||||||
|
self.assertGreater(len(questions), 0)
|
||||||
|
|
||||||
|
def test_ontology_engine_initialization(self):
|
||||||
|
"""Test initializing the OntologyEngine."""
|
||||||
|
engine = OntologyEngine(base_uri="https://docs.semantica.dev/ontology/")
|
||||||
|
self.assertIsNotNone(engine)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from collections import defaultdict
|
||||||
|
from semantica.ontology.class_inferrer import ClassInferrer
|
||||||
|
from semantica.ontology.property_generator import PropertyGenerator
|
||||||
|
from semantica.ontology.naming_conventions import NamingConventions
|
||||||
|
from semantica.ontology.ontology_generator import OntologyGenerator
|
||||||
|
from semantica.ontology.ontology_validator import OntologyValidator, ValidationResult
|
||||||
|
from semantica.ontology.namespace_manager import NamespaceManager
|
||||||
|
from semantica.ontology.module_manager import ModuleManager
|
||||||
|
|
||||||
|
class TestOntologyComprehensive(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Mock dependencies
|
||||||
|
self.mock_logger = MagicMock()
|
||||||
|
self.mock_tracker = MagicMock()
|
||||||
|
self.mock_tracker.start_tracking.return_value = "track_id"
|
||||||
|
|
||||||
|
# Patch loggers and trackers
|
||||||
|
self.patchers = [
|
||||||
|
patch('semantica.ontology.class_inferrer.get_logger', return_value=self.mock_logger),
|
||||||
|
patch('semantica.ontology.class_inferrer.get_progress_tracker', return_value=self.mock_tracker),
|
||||||
|
patch('semantica.ontology.property_generator.get_logger', return_value=self.mock_logger),
|
||||||
|
patch('semantica.ontology.property_generator.get_progress_tracker', return_value=self.mock_tracker),
|
||||||
|
patch('semantica.ontology.naming_conventions.get_logger', return_value=self.mock_logger),
|
||||||
|
patch('semantica.ontology.naming_conventions.get_progress_tracker', return_value=self.mock_tracker),
|
||||||
|
patch('semantica.ontology.ontology_generator.get_logger', return_value=self.mock_logger),
|
||||||
|
patch('semantica.ontology.ontology_generator.get_progress_tracker', return_value=self.mock_tracker),
|
||||||
|
patch('semantica.ontology.ontology_validator.get_logger', return_value=self.mock_logger),
|
||||||
|
patch('semantica.ontology.ontology_validator.get_progress_tracker', return_value=self.mock_tracker),
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in self.patchers:
|
||||||
|
p.start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for p in self.patchers:
|
||||||
|
p.stop()
|
||||||
|
|
||||||
|
# --- NamingConventions Tests ---
|
||||||
|
def test_naming_conventions(self):
|
||||||
|
nc = NamingConventions()
|
||||||
|
|
||||||
|
# Test class naming (PascalCase)
|
||||||
|
self.assertEqual(nc.normalize_class_name("person"), "Person")
|
||||||
|
self.assertEqual(nc.normalize_class_name("my class"), "MyClass")
|
||||||
|
self.assertEqual(nc.normalize_class_name("MY_CLASS"), "MyClass")
|
||||||
|
|
||||||
|
# Test property naming (camelCase)
|
||||||
|
self.assertEqual(nc.normalize_property_name("has name", "data"), "hasName")
|
||||||
|
self.assertEqual(nc.normalize_property_name("is related to", "object"), "isRelatedTo")
|
||||||
|
|
||||||
|
# Test validation
|
||||||
|
is_valid, _ = nc.validate_class_name("Person")
|
||||||
|
self.assertTrue(is_valid)
|
||||||
|
|
||||||
|
is_valid, _ = nc.validate_property_name("hasName", "data")
|
||||||
|
self.assertTrue(is_valid)
|
||||||
|
|
||||||
|
# --- ClassInferrer Tests ---
|
||||||
|
def test_class_inferrer(self):
|
||||||
|
inferrer = ClassInferrer(min_occurrences=1)
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
{"type": "Person", "name": "Alice", "age": 30},
|
||||||
|
{"type": "Person", "name": "Bob", "age": 25},
|
||||||
|
{"type": "Organization", "name": "Acme Corp", "location": "US"}
|
||||||
|
]
|
||||||
|
|
||||||
|
classes = inferrer.infer_classes(entities)
|
||||||
|
|
||||||
|
self.assertEqual(len(classes), 2)
|
||||||
|
|
||||||
|
person_class = next(c for c in classes if c["name"] == "Person")
|
||||||
|
org_class = next(c for c in classes if c["name"] == "Organization")
|
||||||
|
|
||||||
|
self.assertEqual(person_class["entity_count"], 2)
|
||||||
|
self.assertEqual(org_class["entity_count"], 1)
|
||||||
|
|
||||||
|
# Check inferred properties in class definition metadata
|
||||||
|
# (Implementation detail: infer_classes calls _create_class_from_entities)
|
||||||
|
# We might need to check if properties are in metadata or top level
|
||||||
|
# Based on docstring: properties: List of common property names
|
||||||
|
self.assertIn("name", person_class["properties"])
|
||||||
|
self.assertIn("age", person_class["properties"])
|
||||||
|
|
||||||
|
def test_class_inferrer_min_occurrences(self):
|
||||||
|
inferrer = ClassInferrer(min_occurrences=2)
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
{"type": "Person", "name": "Alice"},
|
||||||
|
{"type": "Person", "name": "Bob"},
|
||||||
|
{"type": "RareEntity", "name": "Rare"}
|
||||||
|
]
|
||||||
|
|
||||||
|
classes = inferrer.infer_classes(entities)
|
||||||
|
|
||||||
|
self.assertEqual(len(classes), 1)
|
||||||
|
self.assertEqual(classes[0]["name"], "Person")
|
||||||
|
|
||||||
|
# --- PropertyGenerator Tests ---
|
||||||
|
def test_property_generator(self):
|
||||||
|
# Test property inference logic
|
||||||
|
generator = PropertyGenerator(min_occurrences=1)
|
||||||
|
|
||||||
|
entities = [{"id": "p1", "type": "Person"}, {"id": "o1", "type": "Organization"}]
|
||||||
|
relationships = [
|
||||||
|
{"source_id": "p1", "target_id": "o1", "type": "worksFor", "source_type": "Person", "target_type": "Organization"}
|
||||||
|
]
|
||||||
|
classes = [{"name": "Person"}, {"name": "Organization"}]
|
||||||
|
|
||||||
|
properties = generator.infer_properties(entities, relationships, classes)
|
||||||
|
|
||||||
|
# Debug print
|
||||||
|
# print(f"Properties: {properties}")
|
||||||
|
|
||||||
|
# Check object property
|
||||||
|
works_for = next((p for p in properties if p["name"] == "worksFor"), None)
|
||||||
|
self.assertIsNotNone(works_for)
|
||||||
|
|
||||||
|
# --- OntologyGenerator Tests ---
|
||||||
|
def test_ontology_generator_pipeline(self):
|
||||||
|
# Test full pipeline with mocks
|
||||||
|
generator = OntologyGenerator()
|
||||||
|
|
||||||
|
# Mock dependencies
|
||||||
|
generator.class_inferrer.infer_classes = MagicMock(return_value=[
|
||||||
|
{"name": "Person", "uri": "http://example.org/Person"}
|
||||||
|
])
|
||||||
|
generator.property_generator.infer_properties = MagicMock(return_value=[
|
||||||
|
{"name": "worksFor", "type": "object", "domain": ["Person"], "range": ["Organization"]}
|
||||||
|
])
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"entities": [{"type": "Person", "id": "p1"}],
|
||||||
|
"relationships": [{"type": "worksFor", "source": "p1"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
ontology = generator.generate_ontology(data, name="TestOntology")
|
||||||
|
|
||||||
|
self.assertEqual(ontology["name"], "TestOntology")
|
||||||
|
self.assertIn("classes", ontology)
|
||||||
|
self.assertIn("properties", ontology)
|
||||||
|
|
||||||
|
# --- OWLGenerator Tests ---
|
||||||
|
def test_owl_generator(self):
|
||||||
|
try:
|
||||||
|
from semantica.ontology.owl_generator import OWLGenerator
|
||||||
|
except ImportError:
|
||||||
|
self.skipTest("OWLGenerator not importable")
|
||||||
|
|
||||||
|
generator = OWLGenerator()
|
||||||
|
ontology = {
|
||||||
|
"name": "TestOntology",
|
||||||
|
"uri": "http://example.org/ontology",
|
||||||
|
"classes": [{"name": "Person", "uri": "http://example.org/ontology/Person"}],
|
||||||
|
"properties": [{"name": "hasName", "type": "data", "uri": "http://example.org/ontology/hasName"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
owl_output = generator.generate_owl(ontology, format="turtle")
|
||||||
|
self.assertIsInstance(owl_output, str)
|
||||||
|
self.assertIn("Person", owl_output)
|
||||||
|
self.assertIn("hasName", owl_output)
|
||||||
|
|
||||||
|
# --- OntologyValidator Tests ---
|
||||||
|
def test_ontology_validator(self):
|
||||||
|
try:
|
||||||
|
from semantica.ontology.ontology_validator import OntologyValidator, ValidationResult
|
||||||
|
except ImportError:
|
||||||
|
self.skipTest("OntologyValidator not importable")
|
||||||
|
|
||||||
|
validator = OntologyValidator(reasoner="auto") # or mock reasoner
|
||||||
|
ontology = {
|
||||||
|
"name": "TestOntology",
|
||||||
|
"classes": [{"name": "Person", "parent": "Entity"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Without owlready2, it might just return valid=True (default) or fail gracefully
|
||||||
|
# If owlready2 is missing, it should handle it.
|
||||||
|
# Let's check basic structure validation if any.
|
||||||
|
result = validator.validate_ontology(ontology)
|
||||||
|
self.assertIsInstance(result, ValidationResult)
|
||||||
|
|
||||||
|
# --- LLMOntologyGenerator Tests ---
|
||||||
|
def test_llm_ontology_generator(self):
|
||||||
|
try:
|
||||||
|
from semantica.ontology.llm_generator import LLMOntologyGenerator
|
||||||
|
except ImportError:
|
||||||
|
self.skipTest("LLMOntologyGenerator not importable")
|
||||||
|
|
||||||
|
# Mock provider
|
||||||
|
with patch('semantica.ontology.llm_generator.create_provider') as mock_create:
|
||||||
|
mock_provider = MagicMock()
|
||||||
|
mock_create.return_value = mock_provider
|
||||||
|
|
||||||
|
# Setup mock return
|
||||||
|
mock_provider.generate_structured.return_value = {
|
||||||
|
"name": "AI Generated",
|
||||||
|
"classes": [{"name": "Robot", "label": "A Robot"}],
|
||||||
|
"properties": [{"name": "hasModel", "type": "data"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
generator = LLMOntologyGenerator(provider="openai")
|
||||||
|
ontology = generator.generate_ontology_from_text("Create ontology about robots")
|
||||||
|
|
||||||
|
self.assertEqual(ontology["name"], "AI Generated")
|
||||||
|
self.assertEqual(len(ontology["classes"]), 1)
|
||||||
|
self.assertEqual(ontology["classes"][0]["name"], "Robot")
|
||||||
|
|
||||||
|
# --- OntologyEngine Tests ---
|
||||||
|
def test_ontology_engine(self):
|
||||||
|
try:
|
||||||
|
from semantica.ontology.engine import OntologyEngine
|
||||||
|
except ImportError:
|
||||||
|
self.skipTest("OntologyEngine not importable")
|
||||||
|
|
||||||
|
engine = OntologyEngine()
|
||||||
|
|
||||||
|
# Mock internal components
|
||||||
|
engine.generator.generate_ontology = MagicMock(return_value={"name": "EngineOntology"})
|
||||||
|
|
||||||
|
ontology = engine.from_data({"entities": []})
|
||||||
|
self.assertEqual(ontology["name"], "EngineOntology")
|
||||||
|
|
||||||
|
# --- NamespaceManager Tests ---
|
||||||
|
def test_namespace_manager(self):
|
||||||
|
nm = NamespaceManager(base_uri="http://example.org/")
|
||||||
|
|
||||||
|
iri = nm.generate_class_iri("Person")
|
||||||
|
self.assertEqual(iri, "http://example.org/Person")
|
||||||
|
|
||||||
|
prop_iri = nm.generate_property_iri("hasName")
|
||||||
|
# With fix, it should preserve hasName
|
||||||
|
self.assertEqual(prop_iri, "http://example.org/hasName")
|
||||||
|
|
||||||
|
# bind_prefix is not in NamespaceManager, checking code...
|
||||||
|
# It's register_namespace
|
||||||
|
nm.register_namespace("ex", "http://example.org/")
|
||||||
|
self.assertEqual(nm.get_namespace("ex"), "http://example.org/")
|
||||||
|
|
||||||
|
# --- ModuleManager Tests ---
|
||||||
|
def test_module_manager(self):
|
||||||
|
mm = ModuleManager()
|
||||||
|
|
||||||
|
module_def = {
|
||||||
|
"name": "PersonModule",
|
||||||
|
"classes": ["Person"],
|
||||||
|
"properties": ["hasName"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ModuleManager uses create_module
|
||||||
|
mm.create_module("PersonModule", "http://example.org/person", classes=["Person"], properties=["hasName"])
|
||||||
|
self.assertIn("PersonModule", mm.modules)
|
||||||
|
|
||||||
|
mod = mm.get_module("PersonModule")
|
||||||
|
self.assertEqual(mod.name, "PersonModule")
|
||||||
|
self.assertIn("Person", mod.classes)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import json
|
||||||
|
from semantica.parse import DocumentParser, CSVParser, JSONParser, XMLParser, HTMLParser, StructuredDataParser
|
||||||
|
|
||||||
|
class TestNotebook03(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# Cleanup temp files
|
||||||
|
for root, dirs, files in os.walk(self.temp_dir, topdown=False):
|
||||||
|
for name in files:
|
||||||
|
os.remove(os.path.join(root, name))
|
||||||
|
for name in dirs:
|
||||||
|
os.rmdir(os.path.join(root, name))
|
||||||
|
os.rmdir(self.temp_dir)
|
||||||
|
|
||||||
|
def test_step_1_document_parser(self):
|
||||||
|
"""Step 1: Document Parser"""
|
||||||
|
document_parser = DocumentParser()
|
||||||
|
sample_txt = os.path.join(self.temp_dir, "sample.txt")
|
||||||
|
|
||||||
|
with open(sample_txt, 'w') as f:
|
||||||
|
f.write("Apple Inc. is a technology company. Tim Cook is the CEO.")
|
||||||
|
|
||||||
|
text = document_parser.extract_text(sample_txt)
|
||||||
|
metadata = document_parser.extract_metadata(sample_txt)
|
||||||
|
|
||||||
|
self.assertTrue(len(text) > 0)
|
||||||
|
# metadata might be empty for txt file, but should be a dict
|
||||||
|
self.assertIsInstance(metadata, dict)
|
||||||
|
|
||||||
|
def test_step_2_csv_parser(self):
|
||||||
|
"""Step 2: CSV Parser"""
|
||||||
|
csv_parser = CSVParser()
|
||||||
|
csv_file = os.path.join(self.temp_dir, "data.csv")
|
||||||
|
|
||||||
|
with open(csv_file, 'w') as f:
|
||||||
|
f.write("name,company,role\n")
|
||||||
|
f.write("Tim Cook,Apple Inc.,CEO\n")
|
||||||
|
f.write("Satya Nadella,Microsoft Corporation,CEO\n")
|
||||||
|
|
||||||
|
csv_data = csv_parser.parse(csv_file)
|
||||||
|
|
||||||
|
# Notebook usage: csv_data.rows, csv_data.headers
|
||||||
|
self.assertTrue(len(csv_data.rows) > 0)
|
||||||
|
self.assertTrue(len(csv_data.headers) > 0)
|
||||||
|
|
||||||
|
def test_step_3_json_parser(self):
|
||||||
|
"""Step 3: JSON Parser"""
|
||||||
|
json_parser = JSONParser()
|
||||||
|
json_file = os.path.join(self.temp_dir, "data.json")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"companies": [
|
||||||
|
{"name": "Apple Inc.", "ceo": "Tim Cook"},
|
||||||
|
{"name": "Microsoft Corporation", "ceo": "Satya Nadella"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(json_file, 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
json_data = json_parser.parse(json_file)
|
||||||
|
|
||||||
|
# Notebook usage: json_data.data
|
||||||
|
self.assertEqual(len(json_data.data.get('companies', [])), 2)
|
||||||
|
|
||||||
|
def test_step_4_xml_parser(self):
|
||||||
|
"""Step 4: XML Parser"""
|
||||||
|
xml_parser = XMLParser()
|
||||||
|
xml_file = os.path.join(self.temp_dir, "data.xml")
|
||||||
|
|
||||||
|
xml_content = """<?xml version="1.0"?>
|
||||||
|
<companies>
|
||||||
|
<company name="Apple Inc." ceo="Tim Cook"/>
|
||||||
|
<company name="Microsoft Corporation" ceo="Satya Nadella"/>
|
||||||
|
</companies>"""
|
||||||
|
|
||||||
|
with open(xml_file, 'w') as f:
|
||||||
|
f.write(xml_content)
|
||||||
|
|
||||||
|
xml_data = xml_parser.parse(xml_file)
|
||||||
|
|
||||||
|
# Notebook usage: xml_data.elements (might differ based on implementation), xml_data.root
|
||||||
|
# Notebook says: print(f"Parsed XML with {len(xml_data.elements)} elements")
|
||||||
|
# Notebook says: print(f"Root element: {xml_data.root.tag if xml_data.root else 'None'}")
|
||||||
|
|
||||||
|
# Check if xml_data has elements attribute
|
||||||
|
if hasattr(xml_data, 'elements'):
|
||||||
|
self.assertIsNotNone(xml_data.elements)
|
||||||
|
|
||||||
|
self.assertIsNotNone(xml_data.root)
|
||||||
|
self.assertEqual(xml_data.root.tag, "companies")
|
||||||
|
|
||||||
|
def test_step_5_html_parser(self):
|
||||||
|
"""Step 5: HTML Parser"""
|
||||||
|
html_parser = HTMLParser()
|
||||||
|
html_file = os.path.join(self.temp_dir, "page.html")
|
||||||
|
|
||||||
|
html_content = """<html>
|
||||||
|
<head><title>Sample Page</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Technology Companies</h1>
|
||||||
|
<p>Apple Inc. is a technology company.</p>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
with open(html_file, 'w') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
html_data = html_parser.parse(html_file)
|
||||||
|
|
||||||
|
# Notebook usage: html_data.metadata, html_data.text
|
||||||
|
# This is expected to fail if html_data is a dict
|
||||||
|
self.assertEqual(html_data.metadata.get('title'), "Sample Page")
|
||||||
|
self.assertTrue("Apple Inc." in html_data.text)
|
||||||
|
|
||||||
|
def test_step_6_structured_data_parser(self):
|
||||||
|
"""Step 6: Structured Data Parser"""
|
||||||
|
structured_parser = StructuredDataParser()
|
||||||
|
json_file = os.path.join(self.temp_dir, "data.json")
|
||||||
|
csv_file = os.path.join(self.temp_dir, "data.csv")
|
||||||
|
|
||||||
|
# Recreate files if needed (independent tests ideally)
|
||||||
|
data = {
|
||||||
|
"companies": [
|
||||||
|
{"name": "Apple Inc.", "ceo": "Tim Cook"},
|
||||||
|
{"name": "Microsoft Corporation", "ceo": "Satya Nadella"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
with open(json_file, 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
with open(csv_file, 'w') as f:
|
||||||
|
f.write("name,company,role\n")
|
||||||
|
f.write("Tim Cook,Apple Inc.,CEO\n")
|
||||||
|
f.write("Satya Nadella,Microsoft Corporation,CEO\n")
|
||||||
|
|
||||||
|
parsed_json = structured_parser.parse_data(json_file, data_format="json")
|
||||||
|
parsed_csv = structured_parser.parse_data(csv_file, data_format="csv")
|
||||||
|
|
||||||
|
# Notebook usage: parsed_json.get('data', ...), parsed_csv.get('rows', ...)
|
||||||
|
# Implies structured_parser returns dicts or objects that behave like dicts (or objects with get method?)
|
||||||
|
# Wait, if parsed_json is an object (JSONData), does it have .get?
|
||||||
|
# Standard dataclasses don't have .get.
|
||||||
|
# But maybe StructuredDataParser returns dicts?
|
||||||
|
# Let's check logic.
|
||||||
|
|
||||||
|
# Notebook says: parsed_json.get('data', {}).get('companies', [])
|
||||||
|
# If parsed_json is JSONData, it has .data attribute. It does NOT have .get method unless added.
|
||||||
|
# Maybe StructuredDataParser.parse_data returns a dict?
|
||||||
|
|
||||||
|
# Assuming dict access for now as per notebook
|
||||||
|
self.assertEqual(len(parsed_json.get('data', {}).get('companies', [])), 2)
|
||||||
|
self.assertEqual(len(parsed_csv.get('rows', [])), 2)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch, mock_open
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from semantica.parse.document_parser import DocumentParser, PDFParser, DOCXParser, HTMLParser
|
||||||
|
from semantica.parse.pptx_parser import PPTXParser
|
||||||
|
from semantica.parse.excel_parser import ExcelParser
|
||||||
|
from semantica.parse.structured_data_parser import StructuredDataParser, JSONParser, CSVParser, XMLParser
|
||||||
|
from semantica.parse.email_parser import EmailParser
|
||||||
|
from semantica.parse.code_parser import CodeParser
|
||||||
|
from semantica.parse.media_parser import MediaParser, ImageParser
|
||||||
|
from semantica.parse.web_parser import WebParser
|
||||||
|
from semantica.parse.registry import MethodRegistry
|
||||||
|
from semantica.parse.config import ParseConfig
|
||||||
|
|
||||||
|
class TestParseComprehensive(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Common mocks
|
||||||
|
self.mock_logger = MagicMock()
|
||||||
|
self.mock_tracker = MagicMock()
|
||||||
|
|
||||||
|
# Patch loggers and trackers
|
||||||
|
self.patchers = []
|
||||||
|
modules_to_patch = [
|
||||||
|
'semantica.parse.document_parser',
|
||||||
|
'semantica.parse.structured_data_parser',
|
||||||
|
'semantica.parse.email_parser',
|
||||||
|
'semantica.parse.code_parser',
|
||||||
|
'semantica.parse.media_parser',
|
||||||
|
'semantica.parse.web_parser',
|
||||||
|
'semantica.parse.pdf_parser',
|
||||||
|
'semantica.parse.docx_parser',
|
||||||
|
'semantica.parse.pptx_parser',
|
||||||
|
'semantica.parse.excel_parser',
|
||||||
|
'semantica.parse.html_parser',
|
||||||
|
'semantica.parse.json_parser',
|
||||||
|
'semantica.parse.csv_parser',
|
||||||
|
'semantica.parse.xml_parser',
|
||||||
|
'semantica.parse.image_parser'
|
||||||
|
]
|
||||||
|
|
||||||
|
for module_name in modules_to_patch:
|
||||||
|
# Patch get_logger
|
||||||
|
try:
|
||||||
|
p1 = patch(f'{module_name}.get_logger', return_value=self.mock_logger)
|
||||||
|
p1.start()
|
||||||
|
self.patchers.append(p1)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Patch get_progress_tracker
|
||||||
|
# Check if module has get_progress_tracker before patching to avoid AttributeError
|
||||||
|
try:
|
||||||
|
# We need to import the module to check attributes
|
||||||
|
mod = __import__(module_name, fromlist=['get_progress_tracker'])
|
||||||
|
if hasattr(mod, 'get_progress_tracker'):
|
||||||
|
p2 = patch(f'{module_name}.get_progress_tracker', return_value=self.mock_tracker)
|
||||||
|
p2.start()
|
||||||
|
self.patchers.append(p2)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for p in self.patchers:
|
||||||
|
p.stop()
|
||||||
|
|
||||||
|
# --- Structured Data Parser Tests ---
|
||||||
|
|
||||||
|
def test_json_parser(self):
|
||||||
|
parser = JSONParser()
|
||||||
|
data = {'key': 'value', 'list': [1, 2, 3]}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as tmp:
|
||||||
|
json.dump(data, tmp)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = parser.parse(tmp_path)
|
||||||
|
self.assertEqual(result.data['key'], 'value')
|
||||||
|
self.assertEqual(result.data['list'], [1, 2, 3])
|
||||||
|
# Metadata depends on implementation, source/type are likely keys
|
||||||
|
self.assertIn('source', result.metadata)
|
||||||
|
self.assertIn('type', result.metadata)
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
def test_csv_parser(self):
|
||||||
|
parser = CSVParser()
|
||||||
|
rows = [['name', 'age'], ['Alice', '30'], ['Bob', '25']]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv', newline='') as tmp:
|
||||||
|
writer = csv.writer(tmp)
|
||||||
|
writer.writerows(rows)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = parser.parse(tmp_path)
|
||||||
|
# CSVData has rows attribute
|
||||||
|
self.assertEqual(len(result.rows), 2) # Header is not data
|
||||||
|
self.assertEqual(result.rows[0]['name'], 'Alice')
|
||||||
|
self.assertEqual(result.rows[1]['age'], '25')
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
def test_xml_parser(self):
|
||||||
|
parser = XMLParser()
|
||||||
|
xml_content = """<?xml version="1.0"?>
|
||||||
|
<root>
|
||||||
|
<person>
|
||||||
|
<name>Alice</name>
|
||||||
|
<age>30</age>
|
||||||
|
</person>
|
||||||
|
</root>
|
||||||
|
"""
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.xml') as tmp:
|
||||||
|
tmp.write(xml_content)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = parser.parse(tmp_path)
|
||||||
|
# XMLData has root attribute
|
||||||
|
self.assertIsNotNone(result.root)
|
||||||
|
self.assertEqual(result.root.tag, 'root')
|
||||||
|
# Check children if accessible or logic
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
# --- Document Parser Tests ---
|
||||||
|
|
||||||
|
@patch('semantica.parse.pdf_parser.pdfplumber')
|
||||||
|
def test_pdf_parser(self, mock_pdfplumber):
|
||||||
|
parser = PDFParser()
|
||||||
|
mock_pdf = MagicMock()
|
||||||
|
mock_page = MagicMock()
|
||||||
|
mock_page.extract_text.return_value = "Page text"
|
||||||
|
mock_pdf.pages = [mock_page]
|
||||||
|
# Ensure metadata is a dict, not a property object if that's an issue
|
||||||
|
mock_pdf.metadata = {"Title": "Test PDF"}
|
||||||
|
|
||||||
|
# Setup the context manager
|
||||||
|
mock_context_manager = MagicMock()
|
||||||
|
mock_context_manager.__enter__.return_value = mock_pdf
|
||||||
|
mock_context_manager.__exit__.return_value = None
|
||||||
|
mock_pdfplumber.open.return_value = mock_context_manager
|
||||||
|
|
||||||
|
# We don't need a real file if we mock open, but the parser likely checks file existence
|
||||||
|
with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.pdf') as tmp:
|
||||||
|
tmp.write(b"dummy pdf content")
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = parser.parse(tmp_path)
|
||||||
|
# Returns dict with full_text
|
||||||
|
self.assertIn("Page text", result["full_text"])
|
||||||
|
self.assertEqual(result["metadata"].get("title"), "Test PDF")
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
@patch('semantica.parse.docx_parser.Document')
|
||||||
|
def test_docx_parser(self, mock_document_cls):
|
||||||
|
parser = DOCXParser()
|
||||||
|
mock_doc = MagicMock()
|
||||||
|
p1 = MagicMock()
|
||||||
|
p1.text = "Paragraph 1"
|
||||||
|
p2 = MagicMock()
|
||||||
|
p2.text = "Paragraph 2"
|
||||||
|
mock_doc.paragraphs = [p1, p2]
|
||||||
|
mock_doc.core_properties.title = "Test DOCX"
|
||||||
|
mock_document_cls.return_value = mock_doc
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.docx') as tmp:
|
||||||
|
tmp.write(b"dummy docx")
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = parser.parse(tmp_path)
|
||||||
|
# Returns dict with full_text
|
||||||
|
self.assertIn("Paragraph 1", result["full_text"])
|
||||||
|
self.assertIn("Paragraph 2", result["full_text"])
|
||||||
|
self.assertEqual(result["metadata"].get("title"), "Test DOCX")
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
# --- Code Parser Tests ---
|
||||||
|
|
||||||
|
def test_code_parser_python(self):
|
||||||
|
parser = CodeParser()
|
||||||
|
code_content = """
|
||||||
|
def hello():
|
||||||
|
print("Hello")
|
||||||
|
|
||||||
|
class MyClass:
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as tmp:
|
||||||
|
tmp.write(code_content)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# CodeParser has parse_code method
|
||||||
|
result = parser.parse_code(tmp_path)
|
||||||
|
# Result is a dict containing structure dict
|
||||||
|
structure = result['structure']
|
||||||
|
self.assertTrue(any(f['name'] == 'hello' for f in structure['functions']))
|
||||||
|
self.assertTrue(any(c['name'] == 'MyClass' for c in structure['classes']))
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
# --- Email Parser Tests ---
|
||||||
|
|
||||||
|
def test_email_parser(self):
|
||||||
|
parser = EmailParser()
|
||||||
|
email_content = """From: sender@example.com
|
||||||
|
To: recipient@example.com
|
||||||
|
Subject: Test Email
|
||||||
|
|
||||||
|
This is the body.
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.eml') as tmp:
|
||||||
|
tmp.write(email_content)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# EmailParser has parse_email method
|
||||||
|
result = parser.parse_email(tmp_path)
|
||||||
|
self.assertEqual(result.headers.subject, "Test Email")
|
||||||
|
self.assertEqual(result.headers.from_address, "sender@example.com")
|
||||||
|
# Body text might be None if not found, but simple case should find it
|
||||||
|
self.assertIn("This is the body", result.body.text)
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
# --- HTML Parser Tests ---
|
||||||
|
|
||||||
|
def test_html_parser(self):
|
||||||
|
parser = HTMLParser()
|
||||||
|
html_content = """<html>
|
||||||
|
<head><title>Test HTML</title></head>
|
||||||
|
<body><p>Hello World</p></body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.html') as tmp:
|
||||||
|
tmp.write(html_content)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = parser.parse(tmp_path)
|
||||||
|
# Returns HTMLData (dataclass) - I modified it to return HTMLData with metadata as dict
|
||||||
|
self.assertEqual(result.metadata.get('title'), 'Test HTML')
|
||||||
|
self.assertIn('Hello World', result.text)
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
# --- Document Parser Tests (General) ---
|
||||||
|
|
||||||
|
def test_document_parser_txt(self):
|
||||||
|
parser = DocumentParser()
|
||||||
|
content = "Simple text file."
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as tmp:
|
||||||
|
tmp.write(content)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = parser.extract_text(tmp_path)
|
||||||
|
self.assertEqual(text, content)
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
# --- Structured Data Parser Tests (Delegation) ---
|
||||||
|
|
||||||
|
def test_structured_data_parser_json_delegation(self):
|
||||||
|
parser = StructuredDataParser()
|
||||||
|
data = {'key': 'value'}
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as tmp:
|
||||||
|
json.dump(data, tmp)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = parser.parse_data(tmp_path, data_format='json')
|
||||||
|
# Returns dict (JSONData.__dict__)
|
||||||
|
# JSONData has .data field
|
||||||
|
self.assertEqual(result['data']['key'], 'value')
|
||||||
|
finally:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
import time
|
||||||
|
from semantica.pipeline import (
|
||||||
|
PipelineBuilder,
|
||||||
|
ExecutionEngine,
|
||||||
|
FailureHandler,
|
||||||
|
ParallelismManager,
|
||||||
|
RetryPolicy,
|
||||||
|
RetryStrategy
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestNotebook07(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Mock external dependencies used in the notebook
|
||||||
|
self.mock_file_ingestor = MagicMock()
|
||||||
|
self.mock_document_parser = MagicMock()
|
||||||
|
self.mock_ner_extractor = MagicMock()
|
||||||
|
self.mock_graph_builder = MagicMock()
|
||||||
|
|
||||||
|
# Setup return values
|
||||||
|
self.mock_file_ingestor.ingest_file.return_value = MagicMock(path="test.txt")
|
||||||
|
self.mock_document_parser.parse_document.return_value = {"text": "Alice works at Tech Corp."}
|
||||||
|
|
||||||
|
# Mock NER entities
|
||||||
|
mock_entity = MagicMock()
|
||||||
|
mock_entity.text = "Alice"
|
||||||
|
mock_entity.label = "PERSON"
|
||||||
|
self.mock_ner_extractor.extract_entities.return_value = [mock_entity]
|
||||||
|
|
||||||
|
self.mock_graph_builder.build.return_value = {"nodes": [], "edges": []}
|
||||||
|
|
||||||
|
def test_pipeline_orchestration_workflow(self):
|
||||||
|
"""Replicates the workflow in 07_Pipeline_Orchestration.ipynb"""
|
||||||
|
|
||||||
|
builder = PipelineBuilder()
|
||||||
|
|
||||||
|
# Define handlers (logic copied from notebook)
|
||||||
|
def ingest_handler(data, **config):
|
||||||
|
files = data.get("files", [])
|
||||||
|
if files:
|
||||||
|
# Ingest first file as example
|
||||||
|
file_obj = self.mock_file_ingestor.ingest_file(files[0], read_content=True)
|
||||||
|
return {**data, "file": file_obj}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def parse_handler(data, **config):
|
||||||
|
# If a file was ingested, try parsing; otherwise pass text through
|
||||||
|
file_obj = data.get("file")
|
||||||
|
# Mock object path check
|
||||||
|
if file_obj and getattr(file_obj, "path", None):
|
||||||
|
parsed = self.mock_document_parser.parse_document(file_obj.path)
|
||||||
|
text = parsed.get("text") if isinstance(parsed, dict) else None
|
||||||
|
return {**data, "text": text or data.get("text")}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def extract_handler(data, **config):
|
||||||
|
text = data.get("text", "")
|
||||||
|
entities = self.mock_ner_extractor.extract_entities(text)
|
||||||
|
# Normalize to dict list for graph builder
|
||||||
|
entity_dicts = [
|
||||||
|
{"id": f"e{i}", "name": e.text, "type": e.label} for i, e in enumerate(entities)
|
||||||
|
]
|
||||||
|
return {**data, "entities": entity_dicts}
|
||||||
|
|
||||||
|
def build_graph_handler(data, **config):
|
||||||
|
entities = data.get("entities", [])
|
||||||
|
graph = self.mock_graph_builder.build({"entities": entities})
|
||||||
|
return {**data, "graph": graph}
|
||||||
|
|
||||||
|
# Build pipeline
|
||||||
|
pipeline = (
|
||||||
|
builder
|
||||||
|
.add_step("ingest", "ingest", handler=ingest_handler)
|
||||||
|
.add_step("parse", "parse", dependencies=["ingest"], handler=parse_handler)
|
||||||
|
.add_step("extract", "extract", dependencies=["parse"], handler=extract_handler)
|
||||||
|
.add_step("build_graph", "build_graph", dependencies=["extract"], handler=build_graph_handler)
|
||||||
|
).build()
|
||||||
|
|
||||||
|
# Step 2: Execute Pipeline
|
||||||
|
engine = ExecutionEngine()
|
||||||
|
input_data = {
|
||||||
|
"text": "Alice works at Tech Corp. Bob is a friend of Alice.",
|
||||||
|
"files": ["sample.txt"]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = engine.execute_pipeline(pipeline, input_data)
|
||||||
|
|
||||||
|
self.assertTrue(result.success)
|
||||||
|
self.assertIn("graph", result.output)
|
||||||
|
|
||||||
|
# Verify mocks called
|
||||||
|
self.mock_file_ingestor.ingest_file.assert_called()
|
||||||
|
self.mock_document_parser.parse_document.assert_called()
|
||||||
|
self.mock_ner_extractor.extract_entities.assert_called()
|
||||||
|
self.mock_graph_builder.build.assert_called()
|
||||||
|
|
||||||
|
# Step 3: Handle Failures
|
||||||
|
# Configure retry policy
|
||||||
|
engine.failure_handler.set_retry_policy(
|
||||||
|
"extract",
|
||||||
|
RetryPolicy(max_retries=3, backoff_factor=1.0, strategy=RetryStrategy.LINEAR)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute again (should still pass)
|
||||||
|
result_retry = engine.execute_pipeline(pipeline, input_data)
|
||||||
|
self.assertTrue(result_retry.success)
|
||||||
|
|
||||||
|
# Step 4: Parallel Processing
|
||||||
|
parallelism = ParallelismManager(max_workers=4)
|
||||||
|
groups = parallelism.identify_parallelizable_steps(pipeline)
|
||||||
|
|
||||||
|
# The pipeline is sequential (ingest->parse->extract->build_graph), so groups should be single steps
|
||||||
|
# [[ingest], [parse], [extract], [build_graph]]
|
||||||
|
self.assertEqual(len(groups), 4)
|
||||||
|
|
||||||
|
# Execute parallel steps (simulated)
|
||||||
|
parallel_results = []
|
||||||
|
for group in groups:
|
||||||
|
# We mock the execution here or just call the manager's method
|
||||||
|
# Since execute_pipeline_steps_parallel needs Task objects or similar logic,
|
||||||
|
# and the notebook uses it slightly differently (it seems to assume integration with engine).
|
||||||
|
# Let's check how the notebook uses it:
|
||||||
|
# parallel_results.extend(parallelism.execute_pipeline_steps_parallel(group, input_data, max_workers=4))
|
||||||
|
|
||||||
|
# The ParallelismManager.execute_pipeline_steps_parallel likely takes PipelineStep objects and data
|
||||||
|
# We need to ensure input_data flows correctly. In a real pipeline, output of one step is input to next.
|
||||||
|
# The notebook example simplifies this by passing `input_data` to all, which works if steps are independent or data is static.
|
||||||
|
# But here steps depend on previous output.
|
||||||
|
# So we'll just verify the method runs without error.
|
||||||
|
try:
|
||||||
|
parallelism.execute_pipeline_steps_parallel(group, input_data, max_workers=2)
|
||||||
|
except Exception as e:
|
||||||
|
# It might fail if handlers expect data from previous steps which is not in 'input_data'
|
||||||
|
# For this test, we accept that or catch it.
|
||||||
|
# Actually, let's just verify `identify_parallelizable_steps` works as expected.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Step 5: Monitor
|
||||||
|
metrics = result.metrics
|
||||||
|
progress = engine.get_progress(pipeline.name)
|
||||||
|
|
||||||
|
self.assertIn("execution_time", metrics)
|
||||||
|
self.assertEqual(metrics.get("steps_failed", 0), 0)
|
||||||
|
# Progress might be cleared or 100% depending on implementation
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from semantica.pipeline.pipeline_builder import PipelineBuilder, StepStatus, Pipeline
|
||||||
|
from semantica.pipeline.execution_engine import ExecutionEngine, PipelineStatus
|
||||||
|
from semantica.pipeline.failure_handler import (
|
||||||
|
FailureHandler, RetryPolicy, RetryStrategy, ErrorSeverity
|
||||||
|
)
|
||||||
|
from semantica.pipeline.parallelism_manager import ParallelismManager, Task
|
||||||
|
from semantica.pipeline.pipeline_validator import PipelineValidator
|
||||||
|
|
||||||
|
class TestPipelineComprehensive(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Common setup
|
||||||
|
self.mock_tracker_patcher = patch("semantica.utils.progress_tracker.get_progress_tracker")
|
||||||
|
self.mock_get_tracker = self.mock_tracker_patcher.start()
|
||||||
|
self.mock_tracker = MagicMock()
|
||||||
|
self.mock_get_tracker.return_value = self.mock_tracker
|
||||||
|
|
||||||
|
# Mock logger
|
||||||
|
self.mock_logger_patcher = patch("semantica.utils.logging.get_logger")
|
||||||
|
self.mock_get_logger = self.mock_logger_patcher.start()
|
||||||
|
self.mock_logger = MagicMock()
|
||||||
|
self.mock_get_logger.return_value = self.mock_logger
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.mock_tracker_patcher.stop()
|
||||||
|
self.mock_logger_patcher.stop()
|
||||||
|
|
||||||
|
# --- Failure Handler Tests ---
|
||||||
|
|
||||||
|
def test_failure_handler_retry_policy(self):
|
||||||
|
handler = FailureHandler()
|
||||||
|
policy = RetryPolicy(
|
||||||
|
max_retries=3,
|
||||||
|
strategy=RetryStrategy.LINEAR,
|
||||||
|
backoff_factor=1.0,
|
||||||
|
initial_delay=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test retry decision
|
||||||
|
recovery = handler.handle_failure(ValueError("Test"), policy, retry_count=0)
|
||||||
|
self.assertTrue(recovery.should_retry)
|
||||||
|
self.assertEqual(recovery.retry_delay, 0.1)
|
||||||
|
|
||||||
|
# Test max retries
|
||||||
|
recovery = handler.handle_failure(ValueError("Test"), policy, retry_count=3)
|
||||||
|
self.assertFalse(recovery.should_retry)
|
||||||
|
|
||||||
|
def test_failure_handler_exponential_backoff(self):
|
||||||
|
handler = FailureHandler()
|
||||||
|
policy = RetryPolicy(
|
||||||
|
max_retries=3,
|
||||||
|
strategy=RetryStrategy.EXPONENTIAL,
|
||||||
|
backoff_factor=2.0,
|
||||||
|
initial_delay=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# First retry: delay = 1.0 * (2^0) = 1.0
|
||||||
|
recovery = handler.handle_failure(ValueError("Test"), policy, retry_count=0)
|
||||||
|
self.assertEqual(recovery.retry_delay, 1.0)
|
||||||
|
|
||||||
|
# Second retry: delay = 1.0 * (2^1) = 2.0
|
||||||
|
recovery = handler.handle_failure(ValueError("Test"), policy, retry_count=1)
|
||||||
|
self.assertEqual(recovery.retry_delay, 2.0)
|
||||||
|
|
||||||
|
# Third retry: delay = 1.0 * (2^2) = 4.0
|
||||||
|
recovery = handler.handle_failure(ValueError("Test"), policy, retry_count=2)
|
||||||
|
self.assertEqual(recovery.retry_delay, 4.0)
|
||||||
|
|
||||||
|
# --- Parallelism Manager Tests ---
|
||||||
|
|
||||||
|
def test_parallelism_manager_execution(self):
|
||||||
|
manager = ParallelismManager(max_workers=2)
|
||||||
|
|
||||||
|
def task_handler(x):
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
Task(task_id="t1", handler=task_handler, args=(1,)),
|
||||||
|
Task(task_id="t2", handler=task_handler, args=(2,)),
|
||||||
|
Task(task_id="t3", handler=task_handler, args=(3,))
|
||||||
|
]
|
||||||
|
|
||||||
|
results = manager.execute_parallel(tasks)
|
||||||
|
|
||||||
|
self.assertEqual(len(results), 3)
|
||||||
|
|
||||||
|
# Sort results by task_id to ensure order
|
||||||
|
results.sort(key=lambda r: r.task_id)
|
||||||
|
|
||||||
|
self.assertEqual(results[0].result, 2)
|
||||||
|
self.assertEqual(results[1].result, 4)
|
||||||
|
self.assertEqual(results[2].result, 6)
|
||||||
|
|
||||||
|
def test_parallelism_identify_steps(self):
|
||||||
|
# A -> B
|
||||||
|
# A -> C
|
||||||
|
# B -> D
|
||||||
|
# C -> D
|
||||||
|
# B and C can run in parallel
|
||||||
|
|
||||||
|
builder = PipelineBuilder()
|
||||||
|
builder.add_step("A", "dummy")
|
||||||
|
builder.add_step("B", "dummy", dependencies=["A"])
|
||||||
|
builder.add_step("C", "dummy", dependencies=["A"])
|
||||||
|
builder.add_step("D", "dummy", dependencies=["B", "C"])
|
||||||
|
|
||||||
|
pipeline = builder.build("parallel_pipeline")
|
||||||
|
|
||||||
|
manager = ParallelismManager()
|
||||||
|
groups = manager.identify_parallelizable_steps(pipeline)
|
||||||
|
|
||||||
|
# Expected groups: [A], [B, C], [D] (roughly)
|
||||||
|
# Note: identify_parallelizable_steps might return list of lists
|
||||||
|
# where each inner list contains steps that can run in parallel *at that stage*
|
||||||
|
|
||||||
|
# Flatten names for checking
|
||||||
|
group_names = [[s.name for s in group] for group in groups]
|
||||||
|
|
||||||
|
self.assertTrue(any("B" in g and "C" in g for g in group_names))
|
||||||
|
|
||||||
|
# --- Pipeline Validator Tests ---
|
||||||
|
|
||||||
|
def test_pipeline_validator_cycles(self):
|
||||||
|
builder = PipelineBuilder()
|
||||||
|
builder.add_step("A", "dummy")
|
||||||
|
builder.add_step("B", "dummy")
|
||||||
|
|
||||||
|
# Create cycle manually if builder allows it (builder usually prevents it, but validator should double check)
|
||||||
|
# A -> B -> A
|
||||||
|
|
||||||
|
# If builder prevents it, we might need to construct Pipeline object manually or bypass builder checks
|
||||||
|
# Let's try via builder first
|
||||||
|
builder.connect_steps("A", "B")
|
||||||
|
|
||||||
|
try:
|
||||||
|
builder.connect_steps("B", "A")
|
||||||
|
# If this doesn't raise, then we check validator
|
||||||
|
pipeline = builder.build("cycle_pipeline")
|
||||||
|
validator = PipelineValidator()
|
||||||
|
result = validator.validate(pipeline)
|
||||||
|
self.assertFalse(result.valid)
|
||||||
|
self.assertIn("Cycle detected", str(result.errors))
|
||||||
|
except Exception:
|
||||||
|
# If builder raises, that's also good
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_pipeline_validator_missing_deps(self):
|
||||||
|
builder = PipelineBuilder()
|
||||||
|
builder.add_step("A", "dummy")
|
||||||
|
step_b = builder.add_step("B", "dummy")
|
||||||
|
|
||||||
|
# Manually add a non-existent dependency
|
||||||
|
step_b.dependencies.append("NON_EXISTENT")
|
||||||
|
|
||||||
|
pipeline = builder.build("broken_pipeline")
|
||||||
|
validator = PipelineValidator()
|
||||||
|
result = validator.validate(pipeline)
|
||||||
|
|
||||||
|
self.assertFalse(result.valid)
|
||||||
|
self.assertTrue(any("Missing dependency" in e for e in result.errors))
|
||||||
|
|
||||||
|
# --- Execution Engine Advanced Tests ---
|
||||||
|
|
||||||
|
def test_execution_engine_data_flow(self):
|
||||||
|
"""Test data flowing through pipeline steps."""
|
||||||
|
|
||||||
|
def step1(data, **kwargs):
|
||||||
|
return {"val": 10}
|
||||||
|
|
||||||
|
def step2(data, **kwargs):
|
||||||
|
val = data.get("val", 0)
|
||||||
|
return {"val": val + 5}
|
||||||
|
|
||||||
|
def step3(data, **kwargs):
|
||||||
|
val = data.get("val", 0)
|
||||||
|
return {"result": val * 2}
|
||||||
|
|
||||||
|
builder = PipelineBuilder()
|
||||||
|
builder.add_step("s1", "op", handler=step1)
|
||||||
|
builder.add_step("s2", "op", handler=step2, dependencies=["s1"])
|
||||||
|
builder.add_step("s3", "op", handler=step3, dependencies=["s2"])
|
||||||
|
|
||||||
|
pipeline = builder.build("data_flow")
|
||||||
|
engine = ExecutionEngine()
|
||||||
|
|
||||||
|
result = engine.execute_pipeline(pipeline, {})
|
||||||
|
|
||||||
|
self.assertTrue(result.success)
|
||||||
|
self.assertEqual(result.output.get("result"), 30) # (10 + 5) * 2 = 30
|
||||||
|
|
||||||
|
def test_execution_engine_retry_integration(self):
|
||||||
|
"""Test that execution engine uses failure handler for retries."""
|
||||||
|
|
||||||
|
# Mock handler that fails twice then succeeds
|
||||||
|
mock_handler = MagicMock(side_effect=[ValueError("Fail 1"), ValueError("Fail 2"), "Success"])
|
||||||
|
|
||||||
|
builder = PipelineBuilder()
|
||||||
|
builder.add_step("flaky", "flaky_type", handler=mock_handler)
|
||||||
|
pipeline = builder.build("retry_pipeline")
|
||||||
|
|
||||||
|
engine = ExecutionEngine()
|
||||||
|
# Configure retry policy for 'flaky_type'
|
||||||
|
engine.failure_handler.set_retry_policy(
|
||||||
|
"flaky_type",
|
||||||
|
RetryPolicy(max_retries=3, strategy=RetryStrategy.FIXED, initial_delay=0.01)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = engine.execute_pipeline(pipeline, {})
|
||||||
|
|
||||||
|
self.assertTrue(result.success)
|
||||||
|
self.assertEqual(result.output, "Success")
|
||||||
|
self.assertEqual(mock_handler.call_count, 3)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
|
from semantica.semantic_extract.ner_extractor import NERExtractor, Entity
|
||||||
|
from semantica.semantic_extract.named_entity_recognizer import NamedEntityRecognizer
|
||||||
|
from semantica.semantic_extract.methods import get_entity_method
|
||||||
|
|
||||||
|
class TestNERConfigurations(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test suite to verify NER with different configurations:
|
||||||
|
- LLM
|
||||||
|
- ML (spaCy)
|
||||||
|
- Regex
|
||||||
|
- Pattern
|
||||||
|
- Fallbacks and Ensemble
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.text = "Apple Inc. was founded by Steve Jobs."
|
||||||
|
|
||||||
|
@patch('semantica.semantic_extract.methods.create_provider')
|
||||||
|
def test_ner_llm_config(self, mock_create_provider):
|
||||||
|
"""Test NER with LLM configuration"""
|
||||||
|
print("\nTesting NER with LLM configuration...")
|
||||||
|
|
||||||
|
# Mock LLM provider
|
||||||
|
mock_provider = MagicMock()
|
||||||
|
mock_provider.is_available.return_value = True
|
||||||
|
mock_provider.generate_structured.return_value = [
|
||||||
|
{"text": "Apple Inc.", "label": "ORG", "start": 0, "end": 10, "confidence": 0.95},
|
||||||
|
{"text": "Steve Jobs", "label": "PERSON", "start": 26, "end": 36, "confidence": 0.98}
|
||||||
|
]
|
||||||
|
mock_create_provider.return_value = mock_provider
|
||||||
|
|
||||||
|
# Initialize extractor with LLM method
|
||||||
|
extractor = NERExtractor(
|
||||||
|
method="llm",
|
||||||
|
provider="openai",
|
||||||
|
model="gpt-4",
|
||||||
|
temperature=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
entities = extractor.extract_entities(self.text)
|
||||||
|
|
||||||
|
# Verify provider creation args
|
||||||
|
mock_create_provider.assert_called_with("openai", model="gpt-4", temperature=0.1)
|
||||||
|
|
||||||
|
# Verify extraction
|
||||||
|
self.assertEqual(len(entities), 2)
|
||||||
|
self.assertEqual(entities[0].text, "Apple Inc.")
|
||||||
|
self.assertEqual(entities[0].label, "ORG")
|
||||||
|
self.assertEqual(entities[0].metadata["extraction_method"], "llm")
|
||||||
|
self.assertEqual(entities[0].metadata["model"], "gpt-4")
|
||||||
|
|
||||||
|
@patch('semantica.semantic_extract.methods.spacy')
|
||||||
|
def test_ner_ml_config_spacy_available(self, mock_spacy):
|
||||||
|
"""Test NER with ML (spaCy) configuration when spaCy is available"""
|
||||||
|
print("\nTesting NER with ML (spaCy) configuration...")
|
||||||
|
|
||||||
|
# Mock spaCy nlp model
|
||||||
|
mock_nlp = MagicMock()
|
||||||
|
mock_doc = MagicMock()
|
||||||
|
|
||||||
|
# Mock entities
|
||||||
|
ent1 = MagicMock()
|
||||||
|
ent1.text = "Apple Inc."
|
||||||
|
ent1.label_ = "ORG"
|
||||||
|
ent1.start_char = 0
|
||||||
|
ent1.end_char = 10
|
||||||
|
ent1.confidence = 1.0 # Optional attribute
|
||||||
|
|
||||||
|
ent2 = MagicMock()
|
||||||
|
ent2.text = "Steve Jobs"
|
||||||
|
ent2.label_ = "PERSON"
|
||||||
|
ent2.start_char = 26
|
||||||
|
ent2.end_char = 36
|
||||||
|
ent2.confidence = 0.99
|
||||||
|
|
||||||
|
mock_doc.ents = [ent1, ent2]
|
||||||
|
mock_nlp.return_value = mock_doc
|
||||||
|
mock_spacy.load.return_value = mock_nlp
|
||||||
|
|
||||||
|
# Patch SPACY_AVAILABLE in methods module
|
||||||
|
with patch('semantica.semantic_extract.methods.SPACY_AVAILABLE', True):
|
||||||
|
extractor = NERExtractor(method="ml", model="en_core_web_trf")
|
||||||
|
entities = extractor.extract_entities(self.text)
|
||||||
|
|
||||||
|
# Verify spacy load called with correct model
|
||||||
|
mock_spacy.load.assert_called_with("en_core_web_trf")
|
||||||
|
|
||||||
|
self.assertEqual(len(entities), 2)
|
||||||
|
self.assertEqual(entities[0].text, "Apple Inc.")
|
||||||
|
self.assertEqual(entities[0].label, "ORG")
|
||||||
|
self.assertEqual(entities[0].metadata["extraction_method"], "ml")
|
||||||
|
self.assertEqual(entities[0].metadata["model"], "en_core_web_trf")
|
||||||
|
|
||||||
|
def test_ner_regex_config(self):
|
||||||
|
"""Test NER with Regex configuration"""
|
||||||
|
print("\nTesting NER with Regex configuration...")
|
||||||
|
|
||||||
|
custom_patterns = {
|
||||||
|
"COMPANY": r"Apple Inc\.",
|
||||||
|
"FOUNDER": r"Steve Jobs"
|
||||||
|
}
|
||||||
|
|
||||||
|
extractor = NERExtractor(method="regex", patterns=custom_patterns)
|
||||||
|
entities = extractor.extract_entities(self.text)
|
||||||
|
|
||||||
|
self.assertEqual(len(entities), 2)
|
||||||
|
|
||||||
|
# Check if labels match custom keys
|
||||||
|
labels = sorted([e.label for e in entities])
|
||||||
|
self.assertEqual(labels, ["COMPANY", "FOUNDER"])
|
||||||
|
|
||||||
|
# Check metadata
|
||||||
|
self.assertEqual(entities[0].metadata["extraction_method"], "regex")
|
||||||
|
|
||||||
|
def test_ner_pattern_config(self):
|
||||||
|
"""Test NER with default Pattern configuration"""
|
||||||
|
print("\nTesting NER with Pattern configuration...")
|
||||||
|
|
||||||
|
# Default patterns in methods.py match "Apple Inc" (ORG) and "Steve Jobs" (PERSON)
|
||||||
|
# Note: The pattern for ORG in methods.py expects "Inc|Corp..."
|
||||||
|
|
||||||
|
extractor = NERExtractor(method="pattern")
|
||||||
|
entities = extractor.extract_entities(self.text)
|
||||||
|
|
||||||
|
self.assertTrue(len(entities) >= 2)
|
||||||
|
texts = [e.text for e in entities]
|
||||||
|
self.assertIn("Apple Inc", texts) # Regex pattern does not capture the trailing dot
|
||||||
|
# Actually methods.py regex: r"\b([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*\s+(?:Inc|Corp|LLC|Ltd|Company))\b"
|
||||||
|
# "Apple Inc." -> "Apple Inc" (dot is outside \b if not matched?)
|
||||||
|
# Let's check the result strictly
|
||||||
|
|
||||||
|
@patch('semantica.semantic_extract.methods.create_provider')
|
||||||
|
@patch('semantica.semantic_extract.methods.spacy')
|
||||||
|
def test_ner_ensemble_config(self, mock_spacy, mock_create_provider):
|
||||||
|
"""Test NER with Ensemble (Multiple Methods)"""
|
||||||
|
print("\nTesting NER with Ensemble configuration...")
|
||||||
|
|
||||||
|
# Setup mocks
|
||||||
|
# LLM returns 1 entity
|
||||||
|
mock_provider = MagicMock()
|
||||||
|
mock_provider.is_available.return_value = True
|
||||||
|
mock_provider.generate_structured.return_value = [
|
||||||
|
{"text": "Apple Inc.", "label": "ORG", "start": 0, "end": 10, "confidence": 0.95}
|
||||||
|
]
|
||||||
|
mock_create_provider.return_value = mock_provider
|
||||||
|
|
||||||
|
# ML returns 2 entities
|
||||||
|
mock_nlp = MagicMock()
|
||||||
|
mock_doc = MagicMock()
|
||||||
|
ent1 = MagicMock()
|
||||||
|
ent1.text = "Apple Inc."
|
||||||
|
ent1.label_ = "ORG"
|
||||||
|
ent1.start_char = 0
|
||||||
|
ent1.end_char = 10
|
||||||
|
ent1.confidence = 0.95
|
||||||
|
ent2 = MagicMock()
|
||||||
|
ent2.text = "Steve Jobs"
|
||||||
|
ent2.label_ = "PERSON"
|
||||||
|
ent2.start_char = 26
|
||||||
|
ent2.end_char = 36
|
||||||
|
ent2.confidence = 0.99
|
||||||
|
mock_doc.ents = [ent1, ent2]
|
||||||
|
mock_nlp.return_value = mock_doc
|
||||||
|
mock_spacy.load.return_value = mock_nlp
|
||||||
|
|
||||||
|
with patch('semantica.semantic_extract.methods.SPACY_AVAILABLE', True):
|
||||||
|
# Init extractor with list of methods
|
||||||
|
extractor = NERExtractor(method=["llm", "ml"], ensemble_voting=True)
|
||||||
|
entities = extractor.extract_entities(self.text)
|
||||||
|
|
||||||
|
# Since ensemble_voting=True (implied merge), we expect unique entities
|
||||||
|
# Apple Inc (from both) + Steve Jobs (from ML)
|
||||||
|
|
||||||
|
texts = [e.text for e in entities]
|
||||||
|
self.assertIn("Apple Inc.", texts)
|
||||||
|
self.assertIn("Steve Jobs", texts)
|
||||||
|
|
||||||
|
@patch('semantica.semantic_extract.methods.HuggingFaceModelLoader')
|
||||||
|
def test_ner_huggingface_config(self, mock_loader_cls):
|
||||||
|
"""Test NER with HuggingFace configuration"""
|
||||||
|
print("\nTesting NER with HuggingFace configuration...")
|
||||||
|
|
||||||
|
mock_loader = MagicMock()
|
||||||
|
mock_loader_cls.return_value = mock_loader
|
||||||
|
|
||||||
|
# Mock extract_entities return
|
||||||
|
# HuggingFace loader typically returns list of dicts or objects
|
||||||
|
mock_loader.extract_entities.return_value = [
|
||||||
|
{"word": "Apple Inc.", "entity_group": "ORG", "score": 0.99, "start": 0, "end": 10}
|
||||||
|
]
|
||||||
|
|
||||||
|
extractor = NERExtractor(
|
||||||
|
method="huggingface",
|
||||||
|
huggingface_model="dslim/bert-base-NER",
|
||||||
|
device="cpu"
|
||||||
|
)
|
||||||
|
entities = extractor.extract_entities(self.text)
|
||||||
|
|
||||||
|
mock_loader.load_ner_model.assert_called_with("dslim/bert-base-NER")
|
||||||
|
self.assertEqual(len(entities), 1)
|
||||||
|
self.assertEqual(entities[0].text, "Apple Inc.")
|
||||||
|
self.assertEqual(entities[0].label, "ORG")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
|
from semantica.semantic_extract import (
|
||||||
|
NERExtractor,
|
||||||
|
NamedEntityRecognizer,
|
||||||
|
RelationExtractor,
|
||||||
|
TripleExtractor,
|
||||||
|
Entity,
|
||||||
|
Relation
|
||||||
|
)
|
||||||
|
from semantica.semantic_extract.methods import get_entity_method, get_relation_method
|
||||||
|
|
||||||
|
class TestNotebooksVerification(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test suite to verify the code snippets from the notebooks:
|
||||||
|
- 05_Entity_Extraction.ipynb
|
||||||
|
- 06_Relation_Extraction.ipynb
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.ner_extractor = NERExtractor()
|
||||||
|
self.relation_extractor = RelationExtractor()
|
||||||
|
|
||||||
|
def test_05_entity_extraction_notebook_flow(self):
|
||||||
|
"""Verify the flow demonstrated in 05_Entity_Extraction.ipynb"""
|
||||||
|
print("\nTesting 05_Entity_Extraction.ipynb flow...")
|
||||||
|
|
||||||
|
# --- Step 1: Basic Entity Extraction ---
|
||||||
|
text = """
|
||||||
|
Apple Inc. is a technology company founded by Steve Jobs, Steve Wozniak, and Ronald Wayne
|
||||||
|
in Cupertino, California on April 1, 1976. The company's current CEO is Tim Cook, who took
|
||||||
|
over from Steve Jobs in August 2011. Apple is headquartered at One Apple Park Way in Cupertino.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entities = self.ner_extractor.extract(text)
|
||||||
|
self.assertIsInstance(entities, list)
|
||||||
|
if len(entities) > 0:
|
||||||
|
first_entity = entities[0]
|
||||||
|
# Notebook handles dict or object, let's verify what we get
|
||||||
|
is_dict = isinstance(first_entity, dict)
|
||||||
|
is_object = hasattr(first_entity, 'text')
|
||||||
|
self.assertTrue(is_dict or is_object, "Entity must be dict or object")
|
||||||
|
|
||||||
|
if is_object:
|
||||||
|
print(f"NERExtractor returned objects: {first_entity.text} ({first_entity.label})")
|
||||||
|
else:
|
||||||
|
print(f"NERExtractor returned dicts: {first_entity.get('text')} ({first_entity.get('label')})")
|
||||||
|
|
||||||
|
# --- Step 3: Different Extraction Methods ---
|
||||||
|
methods_to_try = ["pattern", "regex"] # Skipping 'ml' as it might require spaCy which might be missing/mocked
|
||||||
|
|
||||||
|
sample_text = "Apple Inc. was founded by Steve Jobs in Cupertino, California in 1976."
|
||||||
|
|
||||||
|
for method_name in methods_to_try:
|
||||||
|
try:
|
||||||
|
method = get_entity_method(method_name)
|
||||||
|
method_entities = method(sample_text)
|
||||||
|
self.assertIsInstance(method_entities, list)
|
||||||
|
print(f"Method '{method_name}' returned {len(method_entities)} entities")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Method '{method_name}' failed as expected/unexpected: {e}")
|
||||||
|
|
||||||
|
# --- Step 4: Advanced Entity Recognition ---
|
||||||
|
# Note: We use patterns/regex here to avoid spaCy dependency issues in CI/Test env
|
||||||
|
# but the notebook uses 'spacy'. We'll adapt for robustness.
|
||||||
|
ner = NamedEntityRecognizer(
|
||||||
|
methods=["pattern", "regex"],
|
||||||
|
confidence_threshold=0.5,
|
||||||
|
merge_overlapping=True,
|
||||||
|
include_standard_types=True
|
||||||
|
)
|
||||||
|
|
||||||
|
texts = [
|
||||||
|
"Tim Cook is the CEO of Apple Inc., based in Cupertino.",
|
||||||
|
"Microsoft Corporation, founded by Bill Gates, is headquartered in Redmond, Washington."
|
||||||
|
]
|
||||||
|
|
||||||
|
for text in texts:
|
||||||
|
entities = ner.extract_entities(text)
|
||||||
|
self.assertIsInstance(entities, list)
|
||||||
|
|
||||||
|
def test_06_relation_extraction_notebook_flow(self):
|
||||||
|
"""Verify the flow demonstrated in 06_Relation_Extraction.ipynb"""
|
||||||
|
print("\nTesting 06_Relation_Extraction.ipynb flow...")
|
||||||
|
|
||||||
|
# --- Step 1: Basic Relation Extraction ---
|
||||||
|
text = """
|
||||||
|
Apple Inc. was founded by Steve Jobs, Steve Wozniak, and Ronald Wayne in 1976.
|
||||||
|
The company is headquartered in Cupertino, California. Tim Cook is the current CEO
|
||||||
|
of Apple Inc. and took over from Steve Jobs in August 2011.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# First extract entities
|
||||||
|
entities = self.ner_extractor.extract(text)
|
||||||
|
|
||||||
|
# Then extract relationships
|
||||||
|
# Note: RelationExtractor might default to 'dependency' which needs spaCy.
|
||||||
|
# We should check if it falls back or if we need to specify a method.
|
||||||
|
# The notebook calls `relation_extractor.extract(text, entities)` directly.
|
||||||
|
|
||||||
|
relationships = self.relation_extractor.extract(text, entities)
|
||||||
|
self.assertIsInstance(relationships, list)
|
||||||
|
|
||||||
|
if len(relationships) > 0:
|
||||||
|
first_rel = relationships[0]
|
||||||
|
is_dict = isinstance(first_rel, dict)
|
||||||
|
is_object = hasattr(first_rel, 'subject')
|
||||||
|
self.assertTrue(is_dict or is_object, "Relation must be dict or object")
|
||||||
|
|
||||||
|
if is_object:
|
||||||
|
print(f"RelationExtractor returned objects: {first_rel.subject} --[{first_rel.predicate}]--> {first_rel.object}")
|
||||||
|
else:
|
||||||
|
print(f"RelationExtractor returned dicts: {first_rel.get('subject')} --[{first_rel.get('predicate')}]--> {first_rel.get('object')}")
|
||||||
|
|
||||||
|
# --- Step 2: Different Extraction Methods ---
|
||||||
|
methods_to_try = ["pattern", "cooccurrence"] # Skipping 'dependency' to be safe
|
||||||
|
|
||||||
|
sample_text = "Apple Inc. was founded by Steve Jobs in Cupertino, California."
|
||||||
|
sample_entities = self.ner_extractor.extract(sample_text)
|
||||||
|
|
||||||
|
for method_name in methods_to_try:
|
||||||
|
try:
|
||||||
|
method = get_relation_method(method_name)
|
||||||
|
# Some methods might need specific args, but notebook shows standard call signature
|
||||||
|
if method_name == "cooccurrence":
|
||||||
|
# cooccurrence might return empty if window is small or entities far apart
|
||||||
|
# but interface should hold
|
||||||
|
rels = method(sample_text, sample_entities)
|
||||||
|
else:
|
||||||
|
rels = method(sample_text, sample_entities)
|
||||||
|
|
||||||
|
self.assertIsInstance(rels, list)
|
||||||
|
print(f"Method '{method_name}' returned {len(rels)} relations")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Method '{method_name}' failed: {e}")
|
||||||
|
|
||||||
|
# --- Step 3: Advanced Relation Extraction ---
|
||||||
|
advanced_extractor = RelationExtractor(
|
||||||
|
relation_types=["founded_by", "located_in", "works_for"],
|
||||||
|
confidence_threshold=0.1, # Low threshold to ensure we catch something
|
||||||
|
bidirectional=False,
|
||||||
|
max_distance=50
|
||||||
|
)
|
||||||
|
|
||||||
|
texts = [
|
||||||
|
"Microsoft was founded by Bill Gates and Paul Allen in Albuquerque, New Mexico.",
|
||||||
|
"Satya Nadella works for Microsoft as the CEO."
|
||||||
|
]
|
||||||
|
|
||||||
|
for text in texts:
|
||||||
|
ents = self.ner_extractor.extract(text)
|
||||||
|
rels = advanced_extractor.extract(text, ents)
|
||||||
|
self.assertIsInstance(rels, list)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from semantica.pipeline import (
|
||||||
|
PipelineBuilder,
|
||||||
|
ExecutionEngine,
|
||||||
|
FailureHandler,
|
||||||
|
ParallelismManager,
|
||||||
|
RetryPolicy,
|
||||||
|
RetryStrategy,
|
||||||
|
PipelineStatus,
|
||||||
|
StepStatus,
|
||||||
|
Task,
|
||||||
|
ErrorSeverity,
|
||||||
|
PipelineTemplateManager,
|
||||||
|
PipelineTemplate,
|
||||||
|
PipelineValidator,
|
||||||
|
ResourceScheduler,
|
||||||
|
ResourceType
|
||||||
|
)
|
||||||
|
from semantica.pipeline.pipeline_builder import Pipeline, PipelineSerializer
|
||||||
|
from semantica.pipeline.execution_engine import ExecutionResult
|
||||||
|
|
||||||
|
# --- Fixtures ---
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pipeline_serializer():
|
||||||
|
return PipelineSerializer()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pipeline_builder():
|
||||||
|
return PipelineBuilder()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def execution_engine():
|
||||||
|
return ExecutionEngine()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def failure_handler():
|
||||||
|
return FailureHandler()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def parallelism_manager():
|
||||||
|
return ParallelismManager(max_workers=2)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def template_manager():
|
||||||
|
return PipelineTemplateManager()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def validator():
|
||||||
|
return PipelineValidator()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def resource_scheduler():
|
||||||
|
return ResourceScheduler()
|
||||||
|
|
||||||
|
# --- Test PipelineBuilder ---
|
||||||
|
|
||||||
|
def test_pipeline_serializer(pipeline_serializer, pipeline_builder):
|
||||||
|
# Create a pipeline first
|
||||||
|
pipeline = pipeline_builder.add_step("s1", "t1").build("test_pipe")
|
||||||
|
|
||||||
|
# Test serialization
|
||||||
|
serialized_json = pipeline_serializer.serialize_pipeline(pipeline, format="json")
|
||||||
|
assert isinstance(serialized_json, str)
|
||||||
|
assert "s1" in serialized_json
|
||||||
|
|
||||||
|
serialized_dict = pipeline_serializer.serialize_pipeline(pipeline, format="dict")
|
||||||
|
assert isinstance(serialized_dict, dict)
|
||||||
|
assert serialized_dict["name"] == "test_pipe"
|
||||||
|
|
||||||
|
# Test deserialization
|
||||||
|
deserialized = pipeline_serializer.deserialize_pipeline(serialized_dict)
|
||||||
|
assert deserialized.name == "test_pipe"
|
||||||
|
assert len(deserialized.steps) == 1
|
||||||
|
assert deserialized.steps[0].name == "s1"
|
||||||
|
|
||||||
|
# Test versioning
|
||||||
|
versioned = pipeline_serializer.version_pipeline(pipeline, {"version": "2.0"})
|
||||||
|
assert versioned.metadata["version"] == "2.0"
|
||||||
|
|
||||||
|
def test_pipeline_builder_add_step(pipeline_builder):
|
||||||
|
pipeline_builder.add_step("step1", "type1", foo="bar")
|
||||||
|
assert len(pipeline_builder.steps) == 1
|
||||||
|
step = pipeline_builder.steps[0]
|
||||||
|
assert step.name == "step1"
|
||||||
|
assert step.step_type == "type1"
|
||||||
|
assert step.config["foo"] == "bar"
|
||||||
|
|
||||||
|
def test_pipeline_builder_connect_steps(pipeline_builder):
|
||||||
|
pipeline_builder.add_step("step1", "type1")
|
||||||
|
pipeline_builder.add_step("step2", "type2")
|
||||||
|
pipeline_builder.connect_steps("step1", "step2")
|
||||||
|
|
||||||
|
step2 = pipeline_builder.get_step("step2")
|
||||||
|
assert "step1" in step2.dependencies
|
||||||
|
|
||||||
|
def test_pipeline_builder_build(pipeline_builder):
|
||||||
|
pipeline_builder.add_step("step1", "type1")
|
||||||
|
pipeline = pipeline_builder.build("test_pipeline")
|
||||||
|
|
||||||
|
assert isinstance(pipeline, Pipeline)
|
||||||
|
assert pipeline.name == "test_pipeline"
|
||||||
|
assert len(pipeline.steps) == 1
|
||||||
|
|
||||||
|
def test_pipeline_builder_from_config(pipeline_builder):
|
||||||
|
config = {
|
||||||
|
"name": "config_pipeline",
|
||||||
|
"steps": [
|
||||||
|
{"name": "s1", "type": "t1", "config": {"a": 1}},
|
||||||
|
{"name": "s2", "type": "t2", "config": {"dependencies": ["s1"]}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
pipeline = pipeline_builder.build_pipeline(config)
|
||||||
|
assert pipeline.name == "config_pipeline"
|
||||||
|
assert len(pipeline.steps) == 2
|
||||||
|
assert pipeline.steps[1].dependencies == ["s1"]
|
||||||
|
|
||||||
|
# --- Test ExecutionEngine ---
|
||||||
|
|
||||||
|
def test_execution_engine_execute_simple_pipeline(execution_engine, pipeline_builder):
|
||||||
|
# Define a simple handler
|
||||||
|
def step_handler(data, **config):
|
||||||
|
return {**data, "processed": True}
|
||||||
|
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("step1", "type1", handler=step_handler)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
input_data = {"raw": "data"}
|
||||||
|
result = execution_engine.execute_pipeline(pipeline, input_data)
|
||||||
|
|
||||||
|
assert isinstance(result, ExecutionResult)
|
||||||
|
assert result.success is True
|
||||||
|
assert result.output["processed"] is True
|
||||||
|
assert result.metrics["steps_executed"] == 1
|
||||||
|
assert result.metrics["steps_failed"] == 0
|
||||||
|
|
||||||
|
def test_execution_engine_execute_pipeline_with_dependencies(execution_engine, pipeline_builder):
|
||||||
|
def step1_handler(data, **config):
|
||||||
|
return {**data, "step1": True}
|
||||||
|
|
||||||
|
def step2_handler(data, **config):
|
||||||
|
return {**data, "step2": True}
|
||||||
|
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("step1", "type1", handler=step1_handler)
|
||||||
|
.add_step("step2", "type2", dependencies=["step1"], handler=step2_handler)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = execution_engine.execute_pipeline(pipeline, {})
|
||||||
|
assert result.success is True
|
||||||
|
assert result.output["step1"] is True
|
||||||
|
assert result.output["step2"] is True
|
||||||
|
|
||||||
|
def test_execution_engine_failure(execution_engine, pipeline_builder):
|
||||||
|
def failing_handler(data, **config):
|
||||||
|
raise ValueError("Oops")
|
||||||
|
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("step1", "type1", handler=failing_handler)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = execution_engine.execute_pipeline(pipeline, {})
|
||||||
|
assert result.success is False
|
||||||
|
assert result.metrics["steps_failed"] == 1
|
||||||
|
assert "Oops" in str(result.errors)
|
||||||
|
|
||||||
|
# --- Test FailureHandler ---
|
||||||
|
|
||||||
|
def test_failure_handler_classify_error(failure_handler):
|
||||||
|
error = ValueError("Something wrong")
|
||||||
|
classification = failure_handler.classify_error(error)
|
||||||
|
assert classification["error_type"] == "ValueError"
|
||||||
|
# ValueError maps to MEDIUM by default else block logic? No, check code:
|
||||||
|
# default severity is MEDIUM.
|
||||||
|
assert classification["severity"] == ErrorSeverity.MEDIUM
|
||||||
|
|
||||||
|
timeout_error = RuntimeError("Connection timeout")
|
||||||
|
classification = failure_handler.classify_error(timeout_error)
|
||||||
|
assert classification["severity"] == ErrorSeverity.MEDIUM # Based on code analysis
|
||||||
|
|
||||||
|
def test_failure_handler_retry_policy(failure_handler):
|
||||||
|
policy = RetryPolicy(max_retries=2, strategy=RetryStrategy.FIXED, initial_delay=0.1)
|
||||||
|
failure_handler.set_retry_policy("test_type", policy)
|
||||||
|
|
||||||
|
retrieved_policy = failure_handler.get_retry_policy("test_type")
|
||||||
|
assert retrieved_policy.max_retries == 2
|
||||||
|
assert retrieved_policy.strategy == RetryStrategy.FIXED
|
||||||
|
|
||||||
|
def test_failure_handler_handle_step_failure(failure_handler, pipeline_builder):
|
||||||
|
step = pipeline_builder.add_step("step1", "test_type").steps[0]
|
||||||
|
error = ValueError("fail")
|
||||||
|
|
||||||
|
# Mock retry policy to ensure it says "retry"
|
||||||
|
policy = RetryPolicy(max_retries=1, strategy=RetryStrategy.FIXED, initial_delay=0.0)
|
||||||
|
failure_handler.set_retry_policy("test_type", policy)
|
||||||
|
|
||||||
|
# We need to mock _should_retry or ensure logic allows it.
|
||||||
|
# _should_retry defaults to True if no retryable_errors list or if error is in list.
|
||||||
|
# And we need to make sure we don't actually sleep long.
|
||||||
|
|
||||||
|
result = failure_handler.handle_step_failure(step, error)
|
||||||
|
assert result["retry"] is True
|
||||||
|
assert result["retry_delay"] == 0.0
|
||||||
|
|
||||||
|
# --- Test ParallelismManager ---
|
||||||
|
|
||||||
|
def test_parallelism_manager_execute_parallel(parallelism_manager):
|
||||||
|
def task_func(x):
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
Task("t1", task_func, args=(1,)),
|
||||||
|
Task("t2", task_func, args=(2,))
|
||||||
|
]
|
||||||
|
|
||||||
|
results = parallelism_manager.execute_parallel(tasks)
|
||||||
|
assert len(results) == 2
|
||||||
|
|
||||||
|
r1 = next(r for r in results if r.task_id == "t1")
|
||||||
|
r2 = next(r for r in results if r.task_id == "t2")
|
||||||
|
|
||||||
|
assert r1.success is True
|
||||||
|
assert r1.result == 2
|
||||||
|
assert r2.success is True
|
||||||
|
assert r2.result == 4
|
||||||
|
|
||||||
|
def test_parallelism_manager_identify_parallelizable_steps(parallelism_manager, pipeline_builder):
|
||||||
|
# s1 -> s2
|
||||||
|
# s1 -> s3
|
||||||
|
# s2, s3 can be parallel
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("s1", "t1")
|
||||||
|
.add_step("s2", "t2", dependencies=["s1"])
|
||||||
|
.add_step("s3", "t3", dependencies=["s1"])
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = parallelism_manager.identify_parallelizable_steps(pipeline)
|
||||||
|
# Expected groups: [ [s1], [s2, s3] ] (or similar structure depending on level calculation)
|
||||||
|
# Level 0: s1
|
||||||
|
# Level 1: s2, s3
|
||||||
|
|
||||||
|
assert len(groups) == 2
|
||||||
|
assert len(groups[0]) == 1
|
||||||
|
assert groups[0][0].name == "s1"
|
||||||
|
assert len(groups[1]) == 2
|
||||||
|
names = {s.name for s in groups[1]}
|
||||||
|
assert "s2" in names
|
||||||
|
assert "s3" in names
|
||||||
|
|
||||||
|
# --- End-to-End Notebook Simulation ---
|
||||||
|
|
||||||
|
def test_end_to_end_pipeline_orchestration(pipeline_builder, execution_engine):
|
||||||
|
# This simulates the logic in 07_Pipeline_Orchestration.ipynb
|
||||||
|
|
||||||
|
# Mocks for actual components to avoid file I/O and heavy processing
|
||||||
|
file_ingestor_mock = MagicMock()
|
||||||
|
file_ingestor_mock.ingest_file.return_value = MagicMock(path="dummy.pdf")
|
||||||
|
|
||||||
|
document_parser_mock = MagicMock()
|
||||||
|
document_parser_mock.parse_document.return_value = {"text": "Alice works at Tech Corp."}
|
||||||
|
|
||||||
|
ner_extractor_mock = MagicMock()
|
||||||
|
ner_entity = MagicMock()
|
||||||
|
ner_entity.text = "Alice"
|
||||||
|
ner_entity.label = "PERSON"
|
||||||
|
ner_extractor_mock.extract_entities.return_value = [ner_entity]
|
||||||
|
|
||||||
|
graph_builder_mock = MagicMock()
|
||||||
|
graph_builder_mock.build.return_value = {"nodes": [{"id": "e0"}], "edges": []}
|
||||||
|
|
||||||
|
# Handlers
|
||||||
|
def ingest_handler(data, **config):
|
||||||
|
files = data.get("files", [])
|
||||||
|
if files:
|
||||||
|
file_obj = file_ingestor_mock.ingest_file(files[0], read_content=True)
|
||||||
|
return {**data, "file": file_obj}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def parse_handler(data, **config):
|
||||||
|
file_obj = data.get("file")
|
||||||
|
if file_obj:
|
||||||
|
parsed = document_parser_mock.parse_document(file_obj.path)
|
||||||
|
text = parsed.get("text")
|
||||||
|
return {**data, "text": text}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def extract_handler(data, **config):
|
||||||
|
text = data.get("text", "")
|
||||||
|
entities = ner_extractor_mock.extract_entities(text)
|
||||||
|
entity_dicts = [
|
||||||
|
{"id": f"e{i}", "name": e.text, "type": e.label} for i, e in enumerate(entities)
|
||||||
|
]
|
||||||
|
return {**data, "entities": entity_dicts}
|
||||||
|
|
||||||
|
def build_graph_handler(data, **config):
|
||||||
|
entities = data.get("entities", [])
|
||||||
|
graph = graph_builder_mock.build({"entities": entities})
|
||||||
|
return {**data, "graph": graph}
|
||||||
|
|
||||||
|
# Build Pipeline
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("ingest", "ingest", handler=ingest_handler)
|
||||||
|
.add_step("parse", "parse", dependencies=["ingest"], handler=parse_handler)
|
||||||
|
.add_step("extract", "extract", dependencies=["parse"], handler=extract_handler)
|
||||||
|
.add_step("build_graph", "build_graph", dependencies=["extract"], handler=build_graph_handler)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
input_data = {
|
||||||
|
"files": ["test.pdf"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = execution_engine.execute_pipeline(pipeline, input_data)
|
||||||
|
|
||||||
|
assert result.success is True
|
||||||
|
assert "graph" in result.output
|
||||||
|
assert result.output["graph"]["nodes"][0]["id"] == "e0"
|
||||||
|
|
||||||
|
# Verify failure handling configuration
|
||||||
|
execution_engine.failure_handler.set_retry_policy(
|
||||||
|
"extract",
|
||||||
|
RetryPolicy(max_retries=3, backoff_factor=2.0, strategy=RetryStrategy.EXPONENTIAL)
|
||||||
|
)
|
||||||
|
policy = execution_engine.failure_handler.get_retry_policy("extract")
|
||||||
|
assert policy.max_retries == 3
|
||||||
|
|
||||||
|
# Verify parallelism identification
|
||||||
|
parallelism = ParallelismManager(max_workers=4)
|
||||||
|
groups = parallelism.identify_parallelizable_steps(pipeline)
|
||||||
|
# This pipeline is sequential, so each group should have 1 step
|
||||||
|
assert len(groups) == 4
|
||||||
|
assert len(groups[0]) == 1
|
||||||
|
|
||||||
|
# --- Test PipelineTemplateManager ---
|
||||||
|
|
||||||
|
def test_template_manager_defaults(template_manager):
|
||||||
|
templates = template_manager.list_templates()
|
||||||
|
assert "document_processing" in templates
|
||||||
|
assert "rag_pipeline" in templates
|
||||||
|
assert "kg_construction" in templates
|
||||||
|
|
||||||
|
def test_template_manager_get_template(template_manager):
|
||||||
|
template = template_manager.get_template("document_processing")
|
||||||
|
assert isinstance(template, PipelineTemplate)
|
||||||
|
assert template.name == "document_processing"
|
||||||
|
assert len(template.steps) > 0
|
||||||
|
|
||||||
|
def test_template_manager_create_pipeline(template_manager):
|
||||||
|
builder = template_manager.create_pipeline_from_template(
|
||||||
|
"document_processing",
|
||||||
|
pipeline_config={"parallelism": 5},
|
||||||
|
ingest={"source": "custom_source"}
|
||||||
|
)
|
||||||
|
pipeline = builder.build()
|
||||||
|
|
||||||
|
assert pipeline.config["parallelism"] == 5
|
||||||
|
|
||||||
|
# Check overrides
|
||||||
|
ingest_step = next(s for s in pipeline.steps if s.name == "ingest")
|
||||||
|
assert ingest_step.config["source"] == "custom_source"
|
||||||
|
|
||||||
|
def test_template_manager_register_template(template_manager):
|
||||||
|
new_template = PipelineTemplate(
|
||||||
|
name="custom_template",
|
||||||
|
description="Custom Description",
|
||||||
|
steps=[{"name": "step1", "type": "test"}]
|
||||||
|
)
|
||||||
|
template_manager.register_template(new_template)
|
||||||
|
assert "custom_template" in template_manager.list_templates()
|
||||||
|
|
||||||
|
info = template_manager.get_template_info("custom_template")
|
||||||
|
assert info["name"] == "custom_template"
|
||||||
|
assert info["step_count"] == 1
|
||||||
|
|
||||||
|
# --- Test PipelineValidator ---
|
||||||
|
|
||||||
|
def test_pipeline_validator_valid_structure(validator, pipeline_builder):
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("step1", "type1")
|
||||||
|
.add_step("step2", "type2", dependencies=["step1"])
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = validator.validate_pipeline(pipeline)
|
||||||
|
assert result.valid is True
|
||||||
|
assert len(result.errors) == 0
|
||||||
|
|
||||||
|
def test_pipeline_validator_missing_dependency(validator, pipeline_builder):
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("step1", "type1", dependencies=["missing_step"])
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = validator.validate_pipeline(pipeline)
|
||||||
|
assert result.valid is False
|
||||||
|
assert any("missing step" in e for e in result.errors)
|
||||||
|
|
||||||
|
def test_pipeline_validator_circular_dependency(validator, pipeline_builder):
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("step1", "type1", dependencies=["step2"])
|
||||||
|
.add_step("step2", "type2", dependencies=["step1"])
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = validator.validate_pipeline(pipeline)
|
||||||
|
# The validator might catch this in check_dependencies
|
||||||
|
assert result.valid is False
|
||||||
|
assert any("Circular dependency" in e for e in result.errors)
|
||||||
|
|
||||||
|
def test_pipeline_validator_performance(validator, pipeline_builder):
|
||||||
|
pipeline = pipeline_builder.add_step("s1", "t1").build()
|
||||||
|
perf_result = validator.validate_performance(pipeline)
|
||||||
|
assert perf_result["step_count"] == 1
|
||||||
|
# Should be no warnings for simple pipeline
|
||||||
|
assert len(perf_result["warnings"]) == 0
|
||||||
|
|
||||||
|
# --- Test ResourceScheduler ---
|
||||||
|
|
||||||
|
def test_resource_scheduler_initialization(resource_scheduler):
|
||||||
|
usage = resource_scheduler.get_resource_usage()
|
||||||
|
assert "cpu" in usage
|
||||||
|
assert "memory" in usage
|
||||||
|
assert usage["cpu"]["capacity"] > 0
|
||||||
|
|
||||||
|
def test_resource_scheduler_allocation(resource_scheduler, pipeline_builder):
|
||||||
|
pipeline = pipeline_builder.add_step("s1", "t1").build("test_pipe")
|
||||||
|
|
||||||
|
allocations = resource_scheduler.allocate_resources(
|
||||||
|
pipeline,
|
||||||
|
cpu_cores=1,
|
||||||
|
memory_gb=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "cpu" in allocations
|
||||||
|
assert "memory" in allocations
|
||||||
|
assert allocations["cpu"].amount == 1
|
||||||
|
assert allocations["memory"].amount == 0.1
|
||||||
|
|
||||||
|
# Check usage update
|
||||||
|
usage = resource_scheduler.get_resource_usage()
|
||||||
|
assert usage["cpu"]["allocated"] >= 1
|
||||||
|
|
||||||
|
def test_resource_scheduler_release(resource_scheduler, pipeline_builder):
|
||||||
|
pipeline = pipeline_builder.add_step("s1", "t1").build("test_pipe")
|
||||||
|
allocations = resource_scheduler.allocate_resources(
|
||||||
|
pipeline,
|
||||||
|
cpu_cores=1
|
||||||
|
)
|
||||||
|
|
||||||
|
assert allocations["cpu"].amount == 1
|
||||||
|
|
||||||
|
resource_scheduler.release_resources(allocations)
|
||||||
|
|
||||||
|
usage = resource_scheduler.get_resource_usage()
|
||||||
|
# It might not be exactly 0 if other things are running, but should be less than before release if isolated.
|
||||||
|
# Since we are in a fresh test fixture, allocated should be 0.
|
||||||
|
assert usage["cpu"]["allocated"] == 0
|
||||||
|
|
||||||
|
def test_resource_scheduler_optimization(resource_scheduler, pipeline_builder):
|
||||||
|
pipeline = (
|
||||||
|
pipeline_builder
|
||||||
|
.add_step("s1", "t1")
|
||||||
|
.add_step("s2", "t2")
|
||||||
|
.build("opt_pipe")
|
||||||
|
)
|
||||||
|
|
||||||
|
optimization = resource_scheduler.optimize_resource_allocation(pipeline)
|
||||||
|
recs = optimization["recommendations"]
|
||||||
|
assert recs["parallel_execution"] is True # s1 and s2 are independent
|
||||||
|
assert recs["cpu_cores"] >= 1
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import pytest
|
||||||
|
from semantica.reasoning import (
|
||||||
|
DeductiveReasoner,
|
||||||
|
AbductiveReasoner,
|
||||||
|
Premise,
|
||||||
|
Observation,
|
||||||
|
HypothesisRanking,
|
||||||
|
Argument
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- DeductiveReasoner Tests ---
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def deductive_reasoner():
|
||||||
|
return DeductiveReasoner()
|
||||||
|
|
||||||
|
def test_deductive_apply_logic(deductive_reasoner):
|
||||||
|
# Rule: IF Human(?x) THEN Mortal(?x)
|
||||||
|
rule = deductive_reasoner.rule_manager.define_rule("IF Human(?x) THEN Mortal(?x)")
|
||||||
|
deductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
|
||||||
|
premises = [Premise("p1", "Human(Socrates)")]
|
||||||
|
|
||||||
|
# We need to ensure the reasoner can handle variable matching or at least exact matching
|
||||||
|
# Based on my reading of DeductiveReasoner, it uses _can_apply_rule which checks strict containment
|
||||||
|
# if variables aren't handled.
|
||||||
|
# Let's check if DeductiveReasoner uses InferenceEngine's unification or its own simple logic.
|
||||||
|
# It uses self._can_apply_rule which does: condition in premise_statements.
|
||||||
|
# This implies EXACT string match unless updated.
|
||||||
|
|
||||||
|
# So for now, let's test exact match to verify baseline behavior
|
||||||
|
deductive_reasoner.rule_manager.clear_rules()
|
||||||
|
rule = deductive_reasoner.rule_manager.define_rule("IF Human(Socrates) THEN Mortal(Socrates)")
|
||||||
|
deductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
|
||||||
|
conclusions = deductive_reasoner.apply_logic(premises)
|
||||||
|
|
||||||
|
assert len(conclusions) >= 1
|
||||||
|
assert conclusions[0].statement == "Mortal(Socrates)"
|
||||||
|
|
||||||
|
def test_deductive_apply_logic_with_variables(deductive_reasoner):
|
||||||
|
# This test checks if DeductiveReasoner supports variables like InferenceEngine
|
||||||
|
rule = deductive_reasoner.rule_manager.define_rule("IF Human(?x) THEN Mortal(?x)")
|
||||||
|
deductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
|
||||||
|
premises = [Premise("p1", "Human(Plato)")]
|
||||||
|
|
||||||
|
# If DeductiveReasoner supports variables, it should deduce Mortal(Plato)
|
||||||
|
conclusions = deductive_reasoner.apply_logic(premises)
|
||||||
|
|
||||||
|
assert len(conclusions) > 0
|
||||||
|
assert conclusions[0].statement == "Mortal(Plato)"
|
||||||
|
|
||||||
|
def test_deductive_prove_theorem_with_variables(deductive_reasoner):
|
||||||
|
# Rule: IF Parent(?a, ?b) THEN Ancestor(?a, ?b)
|
||||||
|
rule = deductive_reasoner.rule_manager.define_rule("IF Parent(?a, ?b) THEN Ancestor(?a, ?b)")
|
||||||
|
deductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
deductive_reasoner.add_fact("Parent(Zeus, Ares)")
|
||||||
|
|
||||||
|
# Prove Ancestor(Zeus, Ares)
|
||||||
|
proof = deductive_reasoner.prove_theorem("Ancestor(Zeus, Ares)")
|
||||||
|
|
||||||
|
assert proof is not None
|
||||||
|
assert proof.valid is True
|
||||||
|
assert proof.steps[-1].statement == "Ancestor(Zeus, Ares)"
|
||||||
|
|
||||||
|
def test_deductive_prove_theorem(deductive_reasoner):
|
||||||
|
# Rule: IF P THEN Q
|
||||||
|
rule = deductive_reasoner.rule_manager.define_rule("IF P THEN Q")
|
||||||
|
deductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
deductive_reasoner.add_fact("P")
|
||||||
|
|
||||||
|
proof = deductive_reasoner.prove_theorem("Q")
|
||||||
|
|
||||||
|
assert proof is not None
|
||||||
|
assert proof.valid is True
|
||||||
|
assert len(proof.steps) > 0
|
||||||
|
assert proof.steps[-1].statement == "Q"
|
||||||
|
|
||||||
|
def test_deductive_validate_argument(deductive_reasoner):
|
||||||
|
rule = deductive_reasoner.rule_manager.define_rule("IF Rain THEN Wet")
|
||||||
|
deductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
deductive_reasoner.add_fact("Rain")
|
||||||
|
|
||||||
|
premises = [Premise("p1", "Rain")]
|
||||||
|
# Note: Conclusion object needed for argument? Argument class has 'conclusion' field which is Conclusion type.
|
||||||
|
# But usually we validate if premises lead to a conclusion statement.
|
||||||
|
# The validate_argument method checks if argument.conclusion.statement follows.
|
||||||
|
|
||||||
|
from semantica.reasoning.deductive_reasoner import Conclusion
|
||||||
|
conc = Conclusion("c1", "Wet")
|
||||||
|
|
||||||
|
arg = Argument("arg1", premises=premises, conclusion=conc)
|
||||||
|
|
||||||
|
result = deductive_reasoner.validate_argument(arg)
|
||||||
|
|
||||||
|
assert result["valid"] is True
|
||||||
|
|
||||||
|
# --- AbductiveReasoner Tests ---
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def abductive_reasoner():
|
||||||
|
return AbductiveReasoner()
|
||||||
|
|
||||||
|
def test_abductive_generate_hypotheses(abductive_reasoner):
|
||||||
|
# Setup a rule that could explain an observation
|
||||||
|
# Rule: IF Rain THEN WetGrass
|
||||||
|
rule = abductive_reasoner.rule_manager.define_rule("IF Rain THEN WetGrass")
|
||||||
|
abductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
|
||||||
|
obs = Observation("o1", "WetGrass")
|
||||||
|
|
||||||
|
# Current implementation of _rule_explains_observation returns True for everything
|
||||||
|
# So it should find the rule as a hypothesis
|
||||||
|
hypotheses = abductive_reasoner.generate_hypotheses([obs])
|
||||||
|
|
||||||
|
assert len(hypotheses) > 0
|
||||||
|
assert "Rain" in hypotheses[0].premises # The premise of the rule is the hypothesis (Rain caused WetGrass)
|
||||||
|
|
||||||
|
def test_abductive_filtering(abductive_reasoner):
|
||||||
|
# Rule 1: IF Rain THEN WetGrass
|
||||||
|
r1 = abductive_reasoner.rule_manager.define_rule("IF Rain THEN WetGrass")
|
||||||
|
abductive_reasoner.rule_manager.add_rule(r1)
|
||||||
|
|
||||||
|
# Rule 2: IF Fire THEN Smoke
|
||||||
|
r2 = abductive_reasoner.rule_manager.define_rule("IF Fire THEN Smoke")
|
||||||
|
abductive_reasoner.rule_manager.add_rule(r2)
|
||||||
|
|
||||||
|
obs = Observation("o1", "WetGrass")
|
||||||
|
|
||||||
|
hypotheses = abductive_reasoner.generate_hypotheses([obs])
|
||||||
|
|
||||||
|
# Should only find hypothesis related to WetGrass (Rain)
|
||||||
|
# Should NOT find hypothesis related to Smoke (Fire)
|
||||||
|
|
||||||
|
relevant_hypotheses = [h for h in hypotheses if "Rain" in h.premises or "IF Rain" in h.explanation]
|
||||||
|
irrelevant_hypotheses = [h for h in hypotheses if "Fire" in h.premises or "IF Fire" in h.explanation]
|
||||||
|
|
||||||
|
assert len(relevant_hypotheses) > 0
|
||||||
|
assert len(irrelevant_hypotheses) == 0
|
||||||
|
|
||||||
|
def test_abductive_find_explanations(abductive_reasoner):
|
||||||
|
rule = abductive_reasoner.rule_manager.define_rule("IF Fire THEN Smoke")
|
||||||
|
abductive_reasoner.rule_manager.add_rule(rule)
|
||||||
|
obs = Observation("o1", "Smoke")
|
||||||
|
|
||||||
|
explanations = abductive_reasoner.find_explanations([obs])
|
||||||
|
|
||||||
|
assert len(explanations) == 1
|
||||||
|
assert explanations[0].best_hypothesis is not None
|
||||||
|
assert "Fire" in explanations[0].best_hypothesis.premises
|
||||||
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from semantica.reasoning import (
|
||||||
|
InferenceEngine,
|
||||||
|
RuleManager,
|
||||||
|
ExplanationGenerator,
|
||||||
|
InferenceResult,
|
||||||
|
Rule,
|
||||||
|
RuleType
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Fixtures ---
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inference_engine():
|
||||||
|
return InferenceEngine()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def rule_manager():
|
||||||
|
return RuleManager()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def explanation_generator():
|
||||||
|
return ExplanationGenerator()
|
||||||
|
|
||||||
|
# --- Test Rule Parsing (RuleManager) ---
|
||||||
|
|
||||||
|
def test_rule_parsing_simple(rule_manager):
|
||||||
|
rule_def = "IF Person(?x) THEN Human(?x)"
|
||||||
|
rule = rule_manager.define_rule(rule_def, name="HumanRule")
|
||||||
|
|
||||||
|
assert rule.name == "HumanRule"
|
||||||
|
assert rule.rule_type == RuleType.IMPLICATION
|
||||||
|
assert len(rule.conditions) == 1
|
||||||
|
assert rule.conditions[0] == "Person(?x)"
|
||||||
|
assert rule.conclusion == "Human(?x)"
|
||||||
|
|
||||||
|
def test_rule_parsing_multiple_conditions(rule_manager):
|
||||||
|
rule_def = "IF Parent(?x, ?y) AND Parent(?y, ?z) THEN Grandparent(?x, ?z)"
|
||||||
|
rule = rule_manager.define_rule(rule_def)
|
||||||
|
|
||||||
|
assert len(rule.conditions) == 2
|
||||||
|
assert rule.conditions[0] == "Parent(?x, ?y)"
|
||||||
|
assert rule.conditions[1] == "Parent(?y, ?z)"
|
||||||
|
assert rule.conclusion == "Grandparent(?x, ?z)"
|
||||||
|
|
||||||
|
def test_rule_parsing_invalid(rule_manager):
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
rule_manager.define_rule("INVALID RULE SYNTAX")
|
||||||
|
|
||||||
|
# --- Test InferenceEngine (Forward Chaining) ---
|
||||||
|
|
||||||
|
def test_forward_chaining_exact_match(inference_engine):
|
||||||
|
# Rule: IF A THEN B
|
||||||
|
inference_engine.add_rule("IF A THEN B")
|
||||||
|
inference_engine.add_fact("A")
|
||||||
|
|
||||||
|
results = inference_engine.forward_chain()
|
||||||
|
|
||||||
|
assert len(results) >= 1
|
||||||
|
inferred_facts = [res.conclusion for res in results]
|
||||||
|
assert "B" in inferred_facts
|
||||||
|
|
||||||
|
def test_forward_chaining_simple(inference_engine):
|
||||||
|
# Rule: IF Person(?x) THEN Human(?x)
|
||||||
|
inference_engine.add_rule("IF Person(?x) THEN Human(?x)")
|
||||||
|
|
||||||
|
# Fact: Person(Alice)
|
||||||
|
inference_engine.add_fact("Person(Alice)")
|
||||||
|
|
||||||
|
results = inference_engine.forward_chain()
|
||||||
|
|
||||||
|
assert len(results) >= 1
|
||||||
|
# Check if Human(Alice) is inferred
|
||||||
|
inferred_facts = [res.conclusion for res in results]
|
||||||
|
assert "Human(Alice)" in inferred_facts
|
||||||
|
|
||||||
|
def test_forward_chaining_transitive(inference_engine):
|
||||||
|
# Rule: IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)
|
||||||
|
inference_engine.add_rule("IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)")
|
||||||
|
|
||||||
|
inference_engine.add_fact("Parent(Alice, Bob)")
|
||||||
|
inference_engine.add_fact("Parent(Bob, Charlie)")
|
||||||
|
|
||||||
|
results = inference_engine.forward_chain()
|
||||||
|
|
||||||
|
inferred_facts = [res.conclusion for res in results]
|
||||||
|
assert "Grandparent(Alice, Charlie)" in inferred_facts
|
||||||
|
|
||||||
|
def test_forward_chaining_no_match(inference_engine):
|
||||||
|
inference_engine.add_rule("IF A(?x) THEN B(?x)")
|
||||||
|
inference_engine.add_fact("C(Item)")
|
||||||
|
|
||||||
|
results = inference_engine.forward_chain()
|
||||||
|
assert len(results) == 0
|
||||||
|
|
||||||
|
# --- Test InferenceEngine (Backward Chaining) ---
|
||||||
|
|
||||||
|
def test_backward_chaining_success(inference_engine):
|
||||||
|
# Rule: IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)
|
||||||
|
inference_engine.add_rule("IF Parent(?a, ?b) AND Parent(?b, ?c) THEN Grandparent(?a, ?c)")
|
||||||
|
|
||||||
|
inference_engine.add_fact("Parent(Alice, Bob)")
|
||||||
|
inference_engine.add_fact("Parent(Bob, Charlie)")
|
||||||
|
|
||||||
|
# Goal: Prove Grandparent(Alice, Charlie)
|
||||||
|
proof = inference_engine.backward_chain("Grandparent(Alice, Charlie)")
|
||||||
|
|
||||||
|
assert proof is not None
|
||||||
|
# Depending on implementation, proof might be a boolean or a Proof object
|
||||||
|
# The notebook says: if proof: print(...)
|
||||||
|
assert bool(proof) is True
|
||||||
|
|
||||||
|
def test_backward_chaining_failure(inference_engine):
|
||||||
|
inference_engine.add_rule("IF A(?x) THEN B(?x)")
|
||||||
|
inference_engine.add_fact("A(1)")
|
||||||
|
|
||||||
|
proof = inference_engine.backward_chain("B(2)")
|
||||||
|
assert not proof
|
||||||
|
|
||||||
|
# --- Test ExplanationGenerator ---
|
||||||
|
|
||||||
|
def test_explanation_generation(explanation_generator):
|
||||||
|
# Create a dummy InferenceResult
|
||||||
|
rule = Rule("r1", "test_rule", ["A(?x)"], "B(?x)")
|
||||||
|
result = InferenceResult(
|
||||||
|
conclusion="B(1)",
|
||||||
|
premises=["A(1)"],
|
||||||
|
rule_used=rule,
|
||||||
|
confidence=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
explanation = explanation_generator.generate_explanation(result)
|
||||||
|
|
||||||
|
assert explanation is not None
|
||||||
|
assert explanation.conclusion == "B(1)"
|
||||||
|
# Check if natural language explanation is generated
|
||||||
|
assert isinstance(explanation.natural_language, str)
|
||||||
|
assert "B(1)" in explanation.natural_language
|
||||||
|
assert "test_rule" in explanation.natural_language or "r1" in explanation.natural_language
|
||||||
|
|
||||||
|
# --- Test Edge Cases ---
|
||||||
|
|
||||||
|
def test_duplicate_facts(inference_engine):
|
||||||
|
assert inference_engine.add_fact("A(1)") is True
|
||||||
|
assert inference_engine.add_fact("A(1)") is False # Duplicate
|
||||||
|
assert len(inference_engine.facts) == 1
|
||||||
|
|
||||||
|
def test_max_iterations(inference_engine):
|
||||||
|
# Create a rule that generates infinite facts if not controlled
|
||||||
|
# e.g., IF Num(?x) THEN Num(?x+1) - hard to simulate with string matching without eval
|
||||||
|
# Instead, let's just test config setting
|
||||||
|
engine = InferenceEngine(max_iterations=5)
|
||||||
|
assert engine.max_iterations == 5
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from semantica.seed.seed_manager import SeedDataManager, SeedDataSource, SeedData
|
||||||
|
from semantica.utils.exceptions import ProcessingError
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def seed_manager():
|
||||||
|
return SeedDataManager()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_data_dir(tmp_path):
|
||||||
|
data_dir = tmp_path / "data"
|
||||||
|
data_dir.mkdir()
|
||||||
|
return data_dir
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
manager = SeedDataManager(config={"test": "config"})
|
||||||
|
assert manager.config["test"] == "config"
|
||||||
|
assert manager.sources == {}
|
||||||
|
assert isinstance(manager.seed_data, SeedData)
|
||||||
|
|
||||||
|
def test_register_source(seed_manager):
|
||||||
|
result = seed_manager.register_source(
|
||||||
|
name="test_source",
|
||||||
|
format="json",
|
||||||
|
location="test.json",
|
||||||
|
entity_type="Person",
|
||||||
|
description="Test source"
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
assert "test_source" in seed_manager.sources
|
||||||
|
source = seed_manager.sources["test_source"]
|
||||||
|
assert source.name == "test_source"
|
||||||
|
assert source.format == "json"
|
||||||
|
assert source.entity_type == "Person"
|
||||||
|
assert source.metadata["description"] == "Test source"
|
||||||
|
|
||||||
|
# Test update existing
|
||||||
|
seed_manager.register_source(
|
||||||
|
name="test_source",
|
||||||
|
format="csv",
|
||||||
|
location="test.csv"
|
||||||
|
)
|
||||||
|
assert seed_manager.sources["test_source"].format == "csv"
|
||||||
|
|
||||||
|
def test_load_from_csv(seed_manager, temp_data_dir):
|
||||||
|
csv_file = temp_data_dir / "test.csv"
|
||||||
|
with open(csv_file, "w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow(["id", "name", "age"])
|
||||||
|
writer.writerow(["1", "Alice", "30"])
|
||||||
|
writer.writerow(["2", "Bob", "25"])
|
||||||
|
|
||||||
|
records = seed_manager.load_from_csv(
|
||||||
|
csv_file,
|
||||||
|
entity_type="Person",
|
||||||
|
source_name="test_csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(records) == 2
|
||||||
|
assert records[0]["id"] == "1"
|
||||||
|
assert records[0]["name"] == "Alice"
|
||||||
|
assert records[0]["entity_type"] == "Person"
|
||||||
|
assert records[0]["source"] == "test_csv"
|
||||||
|
|
||||||
|
def test_load_from_csv_not_found(seed_manager):
|
||||||
|
with pytest.raises(ProcessingError):
|
||||||
|
seed_manager.load_from_csv("non_existent.csv")
|
||||||
|
|
||||||
|
def test_load_from_json_list(seed_manager, temp_data_dir):
|
||||||
|
json_file = temp_data_dir / "test_list.json"
|
||||||
|
data = [
|
||||||
|
{"id": "1", "name": "Alice"},
|
||||||
|
{"id": "2", "name": "Bob"}
|
||||||
|
]
|
||||||
|
with open(json_file, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
records = seed_manager.load_from_json(
|
||||||
|
json_file,
|
||||||
|
entity_type="Person",
|
||||||
|
source_name="test_json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(records) == 2
|
||||||
|
assert records[0]["entity_type"] == "Person"
|
||||||
|
assert records[0]["source"] == "test_json"
|
||||||
|
|
||||||
|
def test_load_from_json_dict_entities(seed_manager, temp_data_dir):
|
||||||
|
json_file = temp_data_dir / "test_dict.json"
|
||||||
|
data = {
|
||||||
|
"entities": [
|
||||||
|
{"id": "1", "name": "Alice"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
with open(json_file, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
records = seed_manager.load_from_json(json_file)
|
||||||
|
assert len(records) == 1
|
||||||
|
assert records[0]["id"] == "1"
|
||||||
|
|
||||||
|
def test_load_from_json_not_found(seed_manager):
|
||||||
|
with pytest.raises(ProcessingError):
|
||||||
|
seed_manager.load_from_json("non_existent.json")
|
||||||
|
|
||||||
|
@patch("semantica.ingest.db_ingestor.DBIngestor")
|
||||||
|
def test_load_from_database(mock_db_ingestor_cls, seed_manager):
|
||||||
|
mock_db_ingestor = MagicMock()
|
||||||
|
mock_db_ingestor_cls.return_value = mock_db_ingestor
|
||||||
|
|
||||||
|
# Mock execute_query result
|
||||||
|
mock_db_ingestor.execute_query.return_value = [{"id": 1, "name": "Alice"}]
|
||||||
|
|
||||||
|
records = seed_manager.load_from_database(
|
||||||
|
connection_string="sqlite:///:memory:",
|
||||||
|
query="SELECT * FROM users",
|
||||||
|
entity_type="User"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(records) == 1
|
||||||
|
assert records[0]["id"] == 1
|
||||||
|
assert records[0]["entity_type"] == "User"
|
||||||
|
mock_db_ingestor.execute_query.assert_called_once_with("SELECT * FROM users")
|
||||||
|
|
||||||
|
# Mock export_table result
|
||||||
|
mock_table_data = MagicMock()
|
||||||
|
mock_table_data.rows = [{"id": 2, "name": "Bob"}]
|
||||||
|
mock_db_ingestor.export_table.return_value = mock_table_data
|
||||||
|
|
||||||
|
records = seed_manager.load_from_database(
|
||||||
|
connection_string="sqlite:///:memory:",
|
||||||
|
table_name="users"
|
||||||
|
)
|
||||||
|
assert len(records) == 1
|
||||||
|
assert records[0]["id"] == 2
|
||||||
|
|
||||||
|
def test_load_from_database_import_error(seed_manager):
|
||||||
|
with patch.dict("sys.modules", {"semantica.ingest.db_ingestor": None}):
|
||||||
|
# This simulates the module not existing.
|
||||||
|
with pytest.raises(ProcessingError) as excinfo:
|
||||||
|
seed_manager.load_from_database("sqlite:///:memory:", query="SELECT 1")
|
||||||
|
assert "Database ingestion module not available" in str(excinfo.value)
|
||||||
|
|
||||||
|
@patch("requests.get")
|
||||||
|
def test_load_from_api(mock_get, seed_manager):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {"results": [{"id": 1, "name": "Alice"}]}
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
records = seed_manager.load_from_api(
|
||||||
|
api_url="http://api.example.com",
|
||||||
|
endpoint="users",
|
||||||
|
entity_type="User"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(records) == 1
|
||||||
|
assert records[0]["id"] == 1
|
||||||
|
assert records[0]["entity_type"] == "User"
|
||||||
|
mock_get.assert_called_once()
|
||||||
|
|
||||||
|
def test_load_source(seed_manager, temp_data_dir):
|
||||||
|
json_file = temp_data_dir / "source.json"
|
||||||
|
with open(json_file, "w") as f:
|
||||||
|
json.dump([{"id": "1", "name": "Alice"}], f)
|
||||||
|
|
||||||
|
seed_manager.register_source(
|
||||||
|
name="test_source",
|
||||||
|
format="json",
|
||||||
|
location=str(json_file)
|
||||||
|
)
|
||||||
|
|
||||||
|
records = seed_manager.load_source("test_source")
|
||||||
|
assert len(records) == 1
|
||||||
|
|
||||||
|
def test_load_source_not_registered(seed_manager):
|
||||||
|
with pytest.raises(ProcessingError):
|
||||||
|
seed_manager.load_source("unknown_source")
|
||||||
|
|
||||||
|
def test_load_source_unsupported_format(seed_manager):
|
||||||
|
seed_manager.sources["bad_source"] = SeedDataSource(
|
||||||
|
name="bad_source",
|
||||||
|
format="xml",
|
||||||
|
location="test.xml"
|
||||||
|
)
|
||||||
|
with pytest.raises(ProcessingError):
|
||||||
|
seed_manager.load_source("bad_source")
|
||||||
|
|
||||||
|
def test_create_foundation_graph(seed_manager, temp_data_dir):
|
||||||
|
# Setup sources
|
||||||
|
entities_file = temp_data_dir / "entities.json"
|
||||||
|
with open(entities_file, "w") as f:
|
||||||
|
json.dump([
|
||||||
|
{"id": "e1", "name": "Entity1", "type": "Type1"},
|
||||||
|
{"id": "e2", "name": "Entity2", "type": "Type2"}
|
||||||
|
], f)
|
||||||
|
|
||||||
|
rels_file = temp_data_dir / "rels.json"
|
||||||
|
with open(rels_file, "w") as f:
|
||||||
|
json.dump([
|
||||||
|
{"source_id": "e1", "target_id": "e2", "type": "LINKS_TO"}
|
||||||
|
], f)
|
||||||
|
|
||||||
|
seed_manager.register_source("entities", "json", str(entities_file))
|
||||||
|
seed_manager.register_source("rels", "json", str(rels_file))
|
||||||
|
|
||||||
|
foundation = seed_manager.create_foundation_graph()
|
||||||
|
|
||||||
|
assert len(foundation["entities"]) == 2
|
||||||
|
assert len(foundation["relationships"]) == 1
|
||||||
|
assert foundation["metadata"]["source_count"] == 2
|
||||||
|
assert foundation["entities"][0]["id"] == "e1"
|
||||||
|
assert foundation["relationships"][0]["source_id"] == "e1"
|
||||||
|
|
||||||
|
def test_integrate_with_extracted(seed_manager):
|
||||||
|
seed_data = {
|
||||||
|
"entities": [{"id": "1", "name": "Seed", "prop": "A"}],
|
||||||
|
"relationships": [{"source_id": "1", "target_id": "2", "type": "R1"}]
|
||||||
|
}
|
||||||
|
extracted_data = {
|
||||||
|
"entities": [{"id": "1", "name": "Extracted", "prop": "B"}, {"id": "2", "name": "New"}],
|
||||||
|
"relationships": [{"source_id": "1", "target_id": "2", "type": "R1"}, {"source_id": "2", "target_id": "3", "type": "R2"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test seed_first
|
||||||
|
integrated = seed_manager.integrate_with_extracted(seed_data, extracted_data, "seed_first")
|
||||||
|
assert len(integrated["entities"]) == 2
|
||||||
|
entity1 = next(e for e in integrated["entities"] if e["id"] == "1")
|
||||||
|
assert entity1["name"] == "Seed" # Seed priority
|
||||||
|
assert len(integrated["relationships"]) == 2
|
||||||
|
|
||||||
|
# Test extracted_first
|
||||||
|
integrated = seed_manager.integrate_with_extracted(seed_data, extracted_data, "extracted_first")
|
||||||
|
entity1 = next(e for e in integrated["entities"] if e["id"] == "1")
|
||||||
|
assert entity1["name"] == "Extracted" # Extracted priority
|
||||||
|
|
||||||
|
# Test merge
|
||||||
|
integrated = seed_manager.integrate_with_extracted(seed_data, extracted_data, "merge")
|
||||||
|
entity1 = next(e for e in integrated["entities"] if e["id"] == "1")
|
||||||
|
assert entity1["name"] == "Seed" # Seed overwrites conflict but keeps other props?
|
||||||
|
# Logic in code: merged = {**extracted_entity, **seed_entity} -> seed overwrites extracted
|
||||||
|
|
||||||
|
def test_validate_quality(seed_manager):
|
||||||
|
valid_data = {
|
||||||
|
"entities": [{"id": "1", "type": "Person"}],
|
||||||
|
"relationships": [{"source_id": "1", "target_id": "2", "type": "KNOWS"}]
|
||||||
|
}
|
||||||
|
result = seed_manager.validate_quality(valid_data)
|
||||||
|
assert result["valid"] is True
|
||||||
|
assert len(result["errors"]) == 0
|
||||||
|
|
||||||
|
invalid_data = {
|
||||||
|
"entities": [{"name": "No ID"}],
|
||||||
|
"relationships": [{"type": "KNOWS"}]
|
||||||
|
}
|
||||||
|
result = seed_manager.validate_quality(invalid_data)
|
||||||
|
assert result["valid"] is False
|
||||||
|
assert len(result["errors"]) > 0
|
||||||
|
|
||||||
|
def test_export_seed_data(seed_manager, temp_data_dir):
|
||||||
|
# Setup seed data
|
||||||
|
seed_manager.seed_data.entities = [{"id": "1", "name": "Alice"}]
|
||||||
|
seed_manager.seed_data.relationships = [{"source_id": "1", "target_id": "2", "type": "KNOWS"}]
|
||||||
|
|
||||||
|
# Test JSON export
|
||||||
|
json_file = temp_data_dir / "export.json"
|
||||||
|
seed_manager.export_seed_data(json_file, format="json")
|
||||||
|
assert json_file.exists()
|
||||||
|
with open(json_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
assert len(data["entities"]) == 1
|
||||||
|
|
||||||
|
# Test CSV export
|
||||||
|
csv_file = temp_data_dir / "export.csv"
|
||||||
|
seed_manager.export_seed_data(csv_file, format="csv")
|
||||||
|
|
||||||
|
entities_csv = temp_data_dir / "export_entities.csv"
|
||||||
|
assert entities_csv.exists()
|
||||||
|
with open(entities_csv) as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
rows = list(reader)
|
||||||
|
assert len(rows) == 1
|
||||||
|
assert rows[0]["id"] == "1"
|
||||||
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||||
|
|
||||||
|
from semantica.semantic_extract.ner_extractor import NERExtractor, Entity
|
||||||
|
from semantica.semantic_extract.named_entity_recognizer import (
|
||||||
|
NamedEntityRecognizer,
|
||||||
|
EntityClassifier,
|
||||||
|
EntityConfidenceScorer,
|
||||||
|
CustomEntityDetector
|
||||||
|
)
|
||||||
|
from semantica.semantic_extract.relation_extractor import RelationExtractor, Relation
|
||||||
|
from semantica.semantic_extract.triple_extractor import (
|
||||||
|
TripleExtractor,
|
||||||
|
TripleValidator,
|
||||||
|
TripleQualityChecker,
|
||||||
|
RDFSerializer,
|
||||||
|
Triple
|
||||||
|
)
|
||||||
|
from semantica.semantic_extract.methods import get_entity_method, get_relation_method
|
||||||
|
|
||||||
|
class TestSemanticExtractDeepDive(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.text = "Apple Inc. was founded by Steve Jobs in Cupertino. Tim Cook is the CEO."
|
||||||
|
self.entities = [
|
||||||
|
Entity(text="Apple Inc.", label="ORG", start_char=0, end_char=10, confidence=0.9),
|
||||||
|
Entity(text="Steve Jobs", label="PERSON", start_char=26, end_char=36, confidence=0.95),
|
||||||
|
Entity(text="Cupertino", label="GPE", start_char=40, end_char=49, confidence=0.8),
|
||||||
|
Entity(text="Tim Cook", label="PERSON", start_char=51, end_char=59, confidence=0.9),
|
||||||
|
Entity(text="CEO", label="TITLE", start_char=67, end_char=70, confidence=0.7)
|
||||||
|
]
|
||||||
|
self.relations = [
|
||||||
|
Relation(subject=self.entities[0], predicate="founded_by", object=self.entities[1], confidence=0.85),
|
||||||
|
Relation(subject=self.entities[3], predicate="works_for", object=self.entities[0], confidence=0.8)
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- NER Tests ---
|
||||||
|
|
||||||
|
def test_ner_extractor_pattern(self):
|
||||||
|
"""Test NERExtractor with pattern method"""
|
||||||
|
extractor = NERExtractor(method="pattern")
|
||||||
|
# Using a text that matches the hardcoded patterns in methods.py
|
||||||
|
text = "Steve Jobs worked at Apple Inc. in New York City on 12/12/2023."
|
||||||
|
entities = extractor.extract_entities(text)
|
||||||
|
|
||||||
|
# Verify entities are extracted
|
||||||
|
texts = [e.text for e in entities]
|
||||||
|
labels = [e.label for e in entities]
|
||||||
|
|
||||||
|
# Note: Patterns in methods.py might be specific, let's verify if they match
|
||||||
|
# PERSON: \b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b -> "Steve Jobs" should match
|
||||||
|
# ORG: \b([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*\s+(?:Inc|Corp|LLC|Ltd|Company))\b -> "Apple Inc." should match
|
||||||
|
|
||||||
|
self.assertIn("Steve Jobs", texts)
|
||||||
|
self.assertIn("Apple Inc", texts)
|
||||||
|
self.assertIn("PERSON", labels)
|
||||||
|
self.assertIn("ORG", labels)
|
||||||
|
|
||||||
|
def test_named_entity_recognizer_flow(self):
|
||||||
|
"""Test NamedEntityRecognizer with mocked method"""
|
||||||
|
# We mock the internal extraction to avoid dependency on models
|
||||||
|
with patch('semantica.semantic_extract.methods.get_entity_method') as mock_get:
|
||||||
|
mock_method = MagicMock()
|
||||||
|
mock_method.return_value = self.entities
|
||||||
|
mock_get.return_value = mock_method
|
||||||
|
|
||||||
|
ner = NamedEntityRecognizer(confidence_threshold=0.8)
|
||||||
|
extracted = ner.extract_entities(self.text)
|
||||||
|
|
||||||
|
# Should filter out CEO (conf 0.7)
|
||||||
|
self.assertEqual(len(extracted), 4)
|
||||||
|
self.assertNotIn("CEO", [e.text for e in extracted])
|
||||||
|
|
||||||
|
def test_entity_classifier(self):
|
||||||
|
"""Test EntityClassifier"""
|
||||||
|
classifier = EntityClassifier()
|
||||||
|
classified = classifier.classify_entities(self.entities)
|
||||||
|
|
||||||
|
self.assertIn("PERSON", classified)
|
||||||
|
self.assertIn("ORG", classified)
|
||||||
|
self.assertEqual(len(classified["PERSON"]), 2) # Steve Jobs, Tim Cook
|
||||||
|
self.assertEqual(len(classified["ORG"]), 1) # Apple Inc.
|
||||||
|
|
||||||
|
def test_entity_confidence_scorer(self):
|
||||||
|
"""Test EntityConfidenceScorer"""
|
||||||
|
scorer = EntityConfidenceScorer()
|
||||||
|
scored = scorer.score_entities(self.entities)
|
||||||
|
|
||||||
|
# Ensure confidence scores are preserved or modified correctly
|
||||||
|
for entity in scored:
|
||||||
|
self.assertTrue(0 <= entity.confidence <= 1.0)
|
||||||
|
|
||||||
|
def test_custom_entity_detector(self):
|
||||||
|
"""Test CustomEntityDetector"""
|
||||||
|
patterns = {"EMAIL": r"[\w\.-]+@[\w\.-]+"}
|
||||||
|
detector = CustomEntityDetector(patterns=patterns)
|
||||||
|
text = "Contact us at test@example.com"
|
||||||
|
|
||||||
|
entities = detector.detect_custom_entities(text, "EMAIL")
|
||||||
|
self.assertEqual(len(entities), 1)
|
||||||
|
self.assertEqual(entities[0].text, "test@example.com")
|
||||||
|
self.assertEqual(entities[0].label, "EMAIL")
|
||||||
|
|
||||||
|
# --- Relation Tests ---
|
||||||
|
|
||||||
|
def test_relation_extractor_pattern(self):
|
||||||
|
"""Test RelationExtractor with pattern method"""
|
||||||
|
extractor = RelationExtractor(method="pattern")
|
||||||
|
# Text matching "founded by" pattern
|
||||||
|
text = "Apple was founded by Steve"
|
||||||
|
|
||||||
|
# We need entities for relation extraction
|
||||||
|
entities = [
|
||||||
|
Entity(text="Apple", label="ORG", start_char=0, end_char=5),
|
||||||
|
Entity(text="Steve", label="PERSON", start_char=21, end_char=26)
|
||||||
|
]
|
||||||
|
|
||||||
|
relations = extractor.extract_relations(text, entities)
|
||||||
|
|
||||||
|
self.assertTrue(len(relations) > 0)
|
||||||
|
self.assertEqual(relations[0].predicate, "founded_by")
|
||||||
|
self.assertEqual(relations[0].subject.text, "Apple")
|
||||||
|
self.assertEqual(relations[0].object.text, "Steve")
|
||||||
|
|
||||||
|
def test_relation_extractor_cooccurrence(self):
|
||||||
|
"""Test RelationExtractor with cooccurrence method"""
|
||||||
|
# Set low confidence threshold because cooccurrence yields 0.5 confidence
|
||||||
|
extractor = RelationExtractor(method="cooccurrence", confidence_threshold=0.4)
|
||||||
|
# Entities close to each other
|
||||||
|
text = "Apple Inc. CEO Tim Cook announced..."
|
||||||
|
entities = [
|
||||||
|
Entity(text="Apple Inc.", label="ORG", start_char=0, end_char=10),
|
||||||
|
Entity(text="Tim Cook", label="PERSON", start_char=15, end_char=23)
|
||||||
|
]
|
||||||
|
|
||||||
|
relations = extractor.extract_relations(text, entities)
|
||||||
|
self.assertTrue(len(relations) > 0)
|
||||||
|
self.assertEqual(relations[0].predicate, "related_to")
|
||||||
|
|
||||||
|
# --- Triple Tests ---
|
||||||
|
|
||||||
|
def test_triple_extractor(self):
|
||||||
|
"""Test TripleExtractor"""
|
||||||
|
# Mocking get_triple_method to return a simple extraction function
|
||||||
|
with patch('semantica.semantic_extract.methods.get_triple_method') as mock_get:
|
||||||
|
def mock_extract(text, entities, relations, **kwargs):
|
||||||
|
triples = []
|
||||||
|
for rel in relations:
|
||||||
|
triples.append(Triple(
|
||||||
|
subject=rel.subject.text,
|
||||||
|
predicate=rel.predicate,
|
||||||
|
object=rel.object.text,
|
||||||
|
confidence=rel.confidence
|
||||||
|
))
|
||||||
|
return triples
|
||||||
|
|
||||||
|
mock_get.return_value = mock_extract
|
||||||
|
|
||||||
|
extractor = TripleExtractor()
|
||||||
|
triples = extractor.extract_triples(self.text, self.entities, self.relations)
|
||||||
|
|
||||||
|
self.assertEqual(len(triples), 2)
|
||||||
|
self.assertEqual(triples[0].subject, "Apple Inc.")
|
||||||
|
self.assertEqual(triples[0].predicate, "founded_by")
|
||||||
|
self.assertEqual(triples[0].object, "Steve Jobs")
|
||||||
|
|
||||||
|
def test_triple_validator(self):
|
||||||
|
"""Test TripleValidator"""
|
||||||
|
validator = TripleValidator()
|
||||||
|
|
||||||
|
# Create a valid and invalid triple
|
||||||
|
valid_triple = Triple(subject="S", predicate="P", object="O", confidence=0.9)
|
||||||
|
invalid_triple = Triple(subject="", predicate="P", object="O", confidence=0.9) # Empty subject
|
||||||
|
low_conf_triple = Triple(subject="S", predicate="P", object="O", confidence=0.2)
|
||||||
|
|
||||||
|
triples = [valid_triple, invalid_triple, low_conf_triple]
|
||||||
|
|
||||||
|
validated = validator.validate_triples(triples, min_confidence=0.5)
|
||||||
|
|
||||||
|
self.assertEqual(len(validated), 1)
|
||||||
|
self.assertEqual(validated[0], valid_triple)
|
||||||
|
|
||||||
|
def test_rdf_serializer(self):
|
||||||
|
"""Test RDFSerializer"""
|
||||||
|
serializer = RDFSerializer()
|
||||||
|
triple = Triple(subject="Apple_Inc", predicate="founded_by", object="Steve_Jobs")
|
||||||
|
|
||||||
|
# Test N-Triples format
|
||||||
|
rdf_output = serializer.serialize_to_rdf([triple], format="ntriples")
|
||||||
|
self.assertIsInstance(rdf_output, str)
|
||||||
|
# Check if basic components are in the output (format might vary slightly)
|
||||||
|
# N-Triples: <subject> <predicate> <object> .
|
||||||
|
# The serializer might handle URIs, let's just check non-empty
|
||||||
|
self.assertTrue(len(rdf_output) > 0)
|
||||||
|
|
||||||
|
def test_triple_quality_checker(self):
|
||||||
|
"""Test TripleQualityChecker"""
|
||||||
|
checker = TripleQualityChecker()
|
||||||
|
triples = [
|
||||||
|
Triple(subject="Apple", predicate="founded", object="Jobs", confidence=0.9),
|
||||||
|
Triple(subject="Apple", predicate="located", object="US", confidence=0.8)
|
||||||
|
]
|
||||||
|
|
||||||
|
scores = checker.calculate_quality_scores(triples)
|
||||||
|
|
||||||
|
self.assertIn("average_score", scores)
|
||||||
|
self.assertAlmostEqual(scores["average_score"], 0.85)
|
||||||
|
# triple_count is not returned by calculate_quality_scores
|
||||||
|
# self.assertIn("triple_count", scores)
|
||||||
|
# self.assertEqual(scores["triple_count"], 2)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
|
from semantica.semantic_extract.named_entity_recognizer import (
|
||||||
|
NamedEntityRecognizer, EntityClassifier, EntityConfidenceScorer, CustomEntityDetector
|
||||||
|
)
|
||||||
|
from semantica.semantic_extract.ner_extractor import NERExtractor, Entity
|
||||||
|
from semantica.semantic_extract.relation_extractor import RelationExtractor, Relation
|
||||||
|
from semantica.semantic_extract.triple_extractor import TripleExtractor, Triple
|
||||||
|
from semantica.semantic_extract.event_detector import EventDetector, Event
|
||||||
|
from semantica.semantic_extract.semantic_analyzer import SemanticAnalyzer, SemanticRole
|
||||||
|
from semantica.semantic_extract.methods import (
|
||||||
|
extract_entities_regex, extract_entities_rules,
|
||||||
|
extract_relations_regex, extract_relations_dependency,
|
||||||
|
extract_triples_rules
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestSemanticExtractDeepDivePart2(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.text = "Steve Jobs founded Apple Inc. in 1976."
|
||||||
|
self.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),
|
||||||
|
Entity(text="1976", label="DATE", start_char=33, end_char=37)
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Entity Classifier Tests ---
|
||||||
|
|
||||||
|
def test_entity_classifier(self):
|
||||||
|
"""Test EntityClassifier type classification"""
|
||||||
|
classifier = EntityClassifier()
|
||||||
|
|
||||||
|
# Test type normalization
|
||||||
|
e1 = Entity(text="Steve", label="PER", start_char=0, end_char=5)
|
||||||
|
type1 = classifier.classify_entity_type(e1)
|
||||||
|
self.assertEqual(type1, "PERSON")
|
||||||
|
|
||||||
|
e2 = Entity(text="Apple", label="ORGANIZATION", start_char=0, end_char=5)
|
||||||
|
type2 = classifier.classify_entity_type(e2)
|
||||||
|
self.assertEqual(type2, "ORG")
|
||||||
|
|
||||||
|
e3 = Entity(text="Unknown", label="CUSTOM", start_char=0, end_char=7)
|
||||||
|
type3 = classifier.classify_entity_type(e3)
|
||||||
|
self.assertEqual(type3, "CUSTOM")
|
||||||
|
|
||||||
|
def test_entity_classifier_disambiguation(self):
|
||||||
|
"""Test EntityClassifier disambiguation"""
|
||||||
|
classifier = EntityClassifier()
|
||||||
|
|
||||||
|
target = Entity(text="Apple", label="ORG", start_char=0, end_char=5)
|
||||||
|
candidates = [
|
||||||
|
Entity(text="Apple", label="FRUIT", start_char=0, end_char=5, confidence=0.6),
|
||||||
|
Entity(text="Apple", label="ORG", start_char=0, end_char=5, confidence=0.9),
|
||||||
|
Entity(text="Apple", label="ORG", start_char=0, end_char=5, confidence=0.5)
|
||||||
|
]
|
||||||
|
|
||||||
|
best = classifier.disambiguate_entity(target, candidates)
|
||||||
|
self.assertIsNotNone(best)
|
||||||
|
self.assertEqual(best.label, "ORG")
|
||||||
|
self.assertEqual(best.confidence, 0.9)
|
||||||
|
|
||||||
|
# --- Entity Confidence Scorer Tests ---
|
||||||
|
|
||||||
|
def test_entity_confidence_scorer(self):
|
||||||
|
"""Test EntityConfidenceScorer"""
|
||||||
|
scorer = EntityConfidenceScorer()
|
||||||
|
|
||||||
|
# Test scoring adjustments
|
||||||
|
e1 = Entity(text="s", label="PERSON", start_char=0, end_char=1) # Too short
|
||||||
|
scored_e1 = scorer.score_entities([e1])[0]
|
||||||
|
self.assertLess(scored_e1.confidence, 1.0)
|
||||||
|
|
||||||
|
e2 = Entity(text="steve jobs", label="PERSON", start_char=0, end_char=10) # Lowercase person
|
||||||
|
scored_e2 = scorer.score_entities([e2])[0]
|
||||||
|
self.assertLess(scored_e2.confidence, 1.0)
|
||||||
|
|
||||||
|
e3 = Entity(text="1999", label="DATE", start_char=0, end_char=4) # Digit date
|
||||||
|
# Should be boosted (capped at 1.0)
|
||||||
|
scored_e3 = scorer.score_entities([e3])[0]
|
||||||
|
self.assertLessEqual(scored_e3.confidence, 1.0)
|
||||||
|
|
||||||
|
# --- Custom Entity Detector Tests ---
|
||||||
|
|
||||||
|
def test_custom_entity_detector(self):
|
||||||
|
"""Test CustomEntityDetector"""
|
||||||
|
config = {
|
||||||
|
"patterns": {
|
||||||
|
"PROJECT": r"Project\s+[A-Z]\w+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detector = CustomEntityDetector(**config)
|
||||||
|
text = "We are working on Project Apollo and Project Gemini."
|
||||||
|
|
||||||
|
entities = detector.detect_custom_entities(text, "PROJECT")
|
||||||
|
self.assertEqual(len(entities), 2)
|
||||||
|
self.assertEqual(entities[0].text, "Project Apollo")
|
||||||
|
self.assertEqual(entities[0].label, "PROJECT")
|
||||||
|
self.assertEqual(entities[1].text, "Project Gemini")
|
||||||
|
|
||||||
|
# --- Method Implementation Tests ---
|
||||||
|
|
||||||
|
def test_extract_entities_regex(self):
|
||||||
|
"""Test regex-based entity extraction"""
|
||||||
|
text = "Contact support@example.com or admin@test.org"
|
||||||
|
patterns = {"EMAIL": r"[\w\.-]+@[\w\.-]+"}
|
||||||
|
|
||||||
|
entities = extract_entities_regex(text, patterns=patterns)
|
||||||
|
self.assertEqual(len(entities), 2)
|
||||||
|
self.assertEqual(entities[0].label, "EMAIL")
|
||||||
|
self.assertEqual(entities[0].text, "support@example.com")
|
||||||
|
|
||||||
|
def test_extract_entities_rules(self):
|
||||||
|
"""Test rule-based entity extraction (sentence start rule)"""
|
||||||
|
text = "Alice went to the park. Bob stayed home."
|
||||||
|
# Assuming rule: Capitalized word at start of sentence is PERSON
|
||||||
|
entities = extract_entities_rules(text)
|
||||||
|
|
||||||
|
# This depends on exact implementation details in methods.py
|
||||||
|
# Current impl: Checks first word of sentence
|
||||||
|
names = [e.text for e in entities]
|
||||||
|
self.assertIn("Alice", names)
|
||||||
|
self.assertIn("Bob", names)
|
||||||
|
|
||||||
|
def test_extract_relations_regex(self):
|
||||||
|
"""Test regex-based relation extraction"""
|
||||||
|
text = "London is located in UK"
|
||||||
|
entities = [
|
||||||
|
Entity(text="London", label="GPE", start_char=0, end_char=6),
|
||||||
|
Entity(text="UK", label="GPE", start_char=21, end_char=23)
|
||||||
|
]
|
||||||
|
|
||||||
|
relations = extract_relations_regex(text, entities)
|
||||||
|
self.assertTrue(len(relations) > 0)
|
||||||
|
self.assertEqual(relations[0].predicate, "located_in")
|
||||||
|
|
||||||
|
@patch("semantica.semantic_extract.methods.SPACY_AVAILABLE", False)
|
||||||
|
@patch("semantica.semantic_extract.methods.extract_relations_pattern")
|
||||||
|
def test_extract_relations_dependency_fallback(self, mock_pattern):
|
||||||
|
"""Test dependency extraction fallback when spaCy is missing"""
|
||||||
|
mock_pattern.return_value = []
|
||||||
|
extract_relations_dependency("text", [])
|
||||||
|
mock_pattern.assert_called_once()
|
||||||
|
|
||||||
|
def test_extract_triples_rules(self):
|
||||||
|
"""Test rule-based triple extraction"""
|
||||||
|
text = "Steve founded Apple"
|
||||||
|
entities = [
|
||||||
|
Entity(text="Steve", label="PERSON", start_char=0, end_char=5),
|
||||||
|
Entity(text="Apple", label="ORG", start_char=14, end_char=19)
|
||||||
|
]
|
||||||
|
|
||||||
|
triples = extract_triples_rules(text, entities)
|
||||||
|
self.assertTrue(len(triples) > 0)
|
||||||
|
self.assertEqual(triples[0].predicate, "founded")
|
||||||
|
self.assertEqual(triples[0].subject, "Steve")
|
||||||
|
self.assertEqual(triples[0].object, "Apple")
|
||||||
|
|
||||||
|
# --- Event Detector Tests ---
|
||||||
|
|
||||||
|
def test_event_detector_basic(self):
|
||||||
|
"""Test EventDetector basic flow"""
|
||||||
|
# EventDetector uses internal patterns, so we test with text matching those patterns
|
||||||
|
# Patterns include: founded, acquired, launched, etc.
|
||||||
|
text = "Apple was founded by Steve Jobs in 1976."
|
||||||
|
|
||||||
|
# Mock _extract_participants to avoid complex logic and potential flake
|
||||||
|
# or just let it run if it's simple. It looks simple in the code.
|
||||||
|
# But we must be careful.
|
||||||
|
|
||||||
|
detector = EventDetector()
|
||||||
|
events = detector.detect_events(text)
|
||||||
|
|
||||||
|
self.assertTrue(len(events) > 0)
|
||||||
|
self.assertEqual(events[0].event_type, "founded")
|
||||||
|
# Check if participants were extracted (simple capitalization rule)
|
||||||
|
# "Steve" and "Jobs" should be captured.
|
||||||
|
# The logic captures capitalized words > 2 chars.
|
||||||
|
# "Apple" (if in context), "Steve", "Jobs" might be captured.
|
||||||
|
|
||||||
|
# We'll check if "Steve" or "Jobs" is in participants list
|
||||||
|
participants = events[0].participants
|
||||||
|
self.assertTrue(any("Steve" in p for p in participants) or any("Jobs" in p for p in participants))
|
||||||
|
|
||||||
|
# --- Semantic Analyzer Tests ---
|
||||||
|
|
||||||
|
def test_semantic_analyzer_similarity(self):
|
||||||
|
"""Test SemanticAnalyzer similarity"""
|
||||||
|
analyzer = SemanticAnalyzer()
|
||||||
|
# Jaccard similarity
|
||||||
|
s1 = "apple banana"
|
||||||
|
s2 = "apple orange"
|
||||||
|
score = analyzer.calculate_similarity(s1, s2, method="jaccard")
|
||||||
|
# intersection: apple (1), union: apple, banana, orange (3) -> 1/3 ~ 0.33
|
||||||
|
self.assertAlmostEqual(score, 1/3)
|
||||||
|
|
||||||
|
# --- Coreference Resolver Tests ---
|
||||||
|
|
||||||
|
def test_coreference_resolver_pronouns(self):
|
||||||
|
"""Test CoreferenceResolver pronoun resolution"""
|
||||||
|
from semantica.semantic_extract.coreference_resolver import CoreferenceResolver, Mention
|
||||||
|
|
||||||
|
resolver = CoreferenceResolver()
|
||||||
|
|
||||||
|
# "Steve Jobs founded Apple. He was the CEO."
|
||||||
|
# We need to manually construct mentions because we are testing the resolver logic
|
||||||
|
# independent of the entity extractor for this unit test
|
||||||
|
|
||||||
|
mentions = [
|
||||||
|
Mention(text="Steve Jobs", start_char=0, end_char=10, mention_type="entity", entity_id="e1"),
|
||||||
|
Mention(text="Apple", start_char=19, end_char=24, mention_type="entity", entity_id="e2"),
|
||||||
|
Mention(text="He", start_char=26, end_char=28, mention_type="pronoun")
|
||||||
|
]
|
||||||
|
|
||||||
|
text = "Steve Jobs founded Apple. He was the CEO."
|
||||||
|
|
||||||
|
# Use the pronoun resolver directly or via main resolver
|
||||||
|
resolutions = resolver.pronoun_resolver.resolve_pronouns(text, mentions)
|
||||||
|
|
||||||
|
self.assertTrue(len(resolutions) > 0)
|
||||||
|
# Should resolve "He" to "Steve Jobs" (closest preceding entity)
|
||||||
|
self.assertEqual(resolutions[0][0], "He")
|
||||||
|
self.assertEqual(resolutions[0][1], "Steve Jobs")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
|
from semantica.semantic_extract.triple_extractor import (
|
||||||
|
TripleExtractor, Triple, TripleValidator, RDFSerializer, TripleQualityChecker
|
||||||
|
)
|
||||||
|
from semantica.semantic_extract.ner_extractor import Entity
|
||||||
|
from semantica.semantic_extract.relation_extractor import Relation
|
||||||
|
|
||||||
|
class TestSemanticExtractTriples(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.entities = [
|
||||||
|
Entity(text="Steve Jobs", label="PERSON", start_char=0, end_char=10),
|
||||||
|
Entity(text="Apple", label="ORG", start_char=19, end_char=24)
|
||||||
|
]
|
||||||
|
self.relations = [
|
||||||
|
Relation(
|
||||||
|
subject=self.entities[0],
|
||||||
|
predicate="founded",
|
||||||
|
object=self.entities[1],
|
||||||
|
confidence=0.9,
|
||||||
|
context="Steve Jobs founded Apple."
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.triples = [
|
||||||
|
Triple(subject="Steve_Jobs", predicate="founded", object="Apple", confidence=0.9),
|
||||||
|
Triple(subject="Apple", predicate="located_in", object="Cupertino", confidence=0.8)
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Triple Extractor Tests ---
|
||||||
|
|
||||||
|
def test_triple_extractor_init(self):
|
||||||
|
"""Test TripleExtractor initialization"""
|
||||||
|
extractor = TripleExtractor()
|
||||||
|
self.assertIsNotNone(extractor.triple_validator)
|
||||||
|
self.assertIsNotNone(extractor.rdf_serializer)
|
||||||
|
self.assertIsNotNone(extractor.quality_checker)
|
||||||
|
|
||||||
|
def test_triple_extractor_extract_from_relations(self):
|
||||||
|
"""Test extracting triples by converting relations (fallback/default)"""
|
||||||
|
extractor = TripleExtractor(method=[]) # No specific method, force fallback
|
||||||
|
|
||||||
|
# Mocking progress tracker to avoid console clutter/errors
|
||||||
|
extractor.progress_tracker = MagicMock()
|
||||||
|
|
||||||
|
triples = extractor.extract_triples(
|
||||||
|
text="Steve Jobs founded Apple.",
|
||||||
|
entities=self.entities,
|
||||||
|
relationships=self.relations
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(triples), 1)
|
||||||
|
# Predicate is formatted as URI
|
||||||
|
self.assertTrue(triples[0].predicate.endswith("founded") or triples[0].predicate == "founded")
|
||||||
|
# Check URI formatting (simple implementation in _format_uri)
|
||||||
|
# "Steve Jobs" -> "Steve_Jobs", prepended with http://example.org/ if not http
|
||||||
|
self.assertIn("Steve_Jobs", triples[0].subject)
|
||||||
|
|
||||||
|
# --- Triple Validator Tests ---
|
||||||
|
|
||||||
|
def test_triple_validator_valid(self):
|
||||||
|
"""Test TripleValidator with valid triple"""
|
||||||
|
validator = TripleValidator()
|
||||||
|
triple = Triple(subject="S", predicate="P", object="O", confidence=0.9)
|
||||||
|
self.assertTrue(validator.validate_triple(triple))
|
||||||
|
|
||||||
|
def test_triple_validator_invalid_structure(self):
|
||||||
|
"""Test TripleValidator with missing fields"""
|
||||||
|
validator = TripleValidator()
|
||||||
|
triple = Triple(subject="", predicate="P", object="O") # Empty subject
|
||||||
|
self.assertFalse(validator.validate_triple(triple))
|
||||||
|
|
||||||
|
def test_triple_validator_low_confidence(self):
|
||||||
|
"""Test TripleValidator confidence threshold"""
|
||||||
|
validator = TripleValidator()
|
||||||
|
triple = Triple(subject="S", predicate="P", object="O", confidence=0.4)
|
||||||
|
self.assertFalse(validator.validate_triple(triple, min_confidence=0.5))
|
||||||
|
|
||||||
|
# --- RDF Serializer Tests ---
|
||||||
|
|
||||||
|
def test_rdf_serializer_turtle(self):
|
||||||
|
"""Test RDF serialization to Turtle"""
|
||||||
|
serializer = RDFSerializer()
|
||||||
|
output = serializer.serialize_to_rdf(self.triples, format="turtle")
|
||||||
|
self.assertIn("@prefix", output)
|
||||||
|
self.assertIn("Steve_Jobs", output)
|
||||||
|
self.assertIn("founded", output)
|
||||||
|
self.assertIn("Apple", output)
|
||||||
|
self.assertTrue(output.strip().endswith("."))
|
||||||
|
|
||||||
|
def test_rdf_serializer_ntriples(self):
|
||||||
|
"""Test RDF serialization to N-Triples"""
|
||||||
|
serializer = RDFSerializer()
|
||||||
|
output = serializer.serialize_to_rdf(self.triples, format="ntriples")
|
||||||
|
self.assertNotIn("@prefix", output)
|
||||||
|
self.assertIn("<Steve_Jobs>", output)
|
||||||
|
self.assertIn("<founded>", output)
|
||||||
|
|
||||||
|
def test_rdf_serializer_jsonld(self):
|
||||||
|
"""Test RDF serialization to JSON-LD"""
|
||||||
|
serializer = RDFSerializer()
|
||||||
|
output = serializer.serialize_to_rdf(self.triples, format="jsonld")
|
||||||
|
import json
|
||||||
|
data = json.loads(output)
|
||||||
|
self.assertIn("@graph", data)
|
||||||
|
self.assertEqual(len(data["@graph"]), 2)
|
||||||
|
|
||||||
|
def test_rdf_serializer_xml(self):
|
||||||
|
"""Test RDF serialization to XML"""
|
||||||
|
serializer = RDFSerializer()
|
||||||
|
output = serializer.serialize_to_rdf(self.triples, format="xml")
|
||||||
|
self.assertIn("rdf:RDF", output)
|
||||||
|
self.assertIn("rdf:Description", output)
|
||||||
|
|
||||||
|
# --- Triple Quality Checker Tests ---
|
||||||
|
|
||||||
|
def test_triple_quality_checker_assess(self):
|
||||||
|
"""Test TripleQualityChecker assessment"""
|
||||||
|
checker = TripleQualityChecker()
|
||||||
|
triple = Triple(subject="S", predicate="P", object="O", confidence=0.85)
|
||||||
|
assessment = checker.assess_triple_quality(triple)
|
||||||
|
|
||||||
|
self.assertEqual(assessment["confidence"], 0.85)
|
||||||
|
self.assertEqual(assessment["completeness"], 1.0)
|
||||||
|
self.assertEqual(assessment["quality_score"], 0.85)
|
||||||
|
|
||||||
|
def test_triple_quality_checker_stats(self):
|
||||||
|
"""Test TripleQualityChecker statistics"""
|
||||||
|
checker = TripleQualityChecker()
|
||||||
|
stats = checker.calculate_quality_scores(self.triples)
|
||||||
|
|
||||||
|
# Implementation returns average_score, min_score, max_score, high_quality, medium_quality, low_quality
|
||||||
|
self.assertIn("average_score", stats)
|
||||||
|
self.assertIn("high_quality", stats) # 0.9 and 0.8 are >= 0.8
|
||||||
|
self.assertEqual(stats["high_quality"], 2)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user