Compare commits

..
16 Commits
Author SHA1 Message Date
KaifAhmad1 8b6e8608c3 Enhance SeedDataManager with robust CSV/JSON support and improved validation 2025-12-12 12:04:24 +05:30
Mohd Kaif 315e2edb14 Merge pull request #81 from Hawksight-AI/seed
Seed Module Tests: Comprehensive Coverage for SeedDataManager
2025-12-11 22:08:19 +05:30
KaifAhmad1 a93ed8f13a Add comprehensive tests for SeedDataManager 2025-12-11 22:04:36 +05:30
Mohd Kaif 921bf18041 Merge pull request #80 from Hawksight-AI/reasoning
Reasoning Module Enhancement: Variable Unification & Advanced Inference
2025-12-11 21:57:13 +05:30
KaifAhmad1 971b42631e Enhance reasoning module with variable unification and add comprehensive tests 2025-12-11 21:54:16 +05:30
Mohd Kaif 3e4bc8521f Merge pull request #79 from Hawksight-AI/pipeline
Comprehensive Test Suite for Pipeline Orchestration Module
2025-12-11 21:36:16 +05:30
KaifAhmad1 521e2e27d8 Add comprehensive tests for pipeline orchestration module 2025-12-11 21:28:51 +05:30
KaifAhmad1 c8f745cef0 chore: remove PR descriptions and temporary test output files 2025-12-11 20:18:12 +05:30
KaifAhmad1 c307011311 Merge branch 'main' of https://github.com/Hawksight-AI/semantica 2025-12-11 19:34:26 +05:30
KaifAhmad1 79ff296001 Removing unnecessary Files 2025-12-11 19:34:03 +05:30
Mohd Kaif 2a28e833b9 Merge pull request #78 from Hawksight-AI/parse
Comprehensive Testing and Fixes for Parse Module
2025-12-11 18:57:53 +05:30
KaifAhmad1 30cede84c7 feat(parse): deep dive and comprehensive testing of parse module
- Added 	ests/parse/test_parse_comprehensive.py covering all core parsers (CSV, JSON, XML, PDF, DOCX, Code, Email, HTML).
- Added 	ests/parse/test_notebook_03.py to verify the document parsing cookbook.
- Fixed HTMLParser metadata extraction and return type (returning HTMLData with dict metadata).
- Fixed HTMLParser import of get_progress_tracker.
- Fixed StructuredDataParser progress tracker initialization.
- Updated PR description.
2025-12-11 18:53:34 +05:30
Mohd Kaif 9c8d0c032b Merge pull request #77 from Hawksight-AI/ontology
Comprehensive Testing and Bug Fixes for Ontology Module
2025-12-11 18:16:31 +05:30
KaifAhmad1 e0e42dc539 feat(ontology): comprehensive testing and bug fixes for ontology module
- Added comprehensive test suite (test_ontology_comprehensive.py) covering all core classes.
- Added test_notebook_14.py to verify documentation examples.
- Fixed PropertyGenerator to respect min_occurrences config.
- Fixed NamingConventions for singularization (ss endings) and camelCase preservation.
- Fixed OntologyVisualizer to handle list-type domains/ranges.
- Fixed ModuleManager method usage in tests.
- Validated all 32 tests pass.
2025-12-11 18:11:58 +05:30
Mohd Kaif f59fe1d689 Merge pull request #76 from Hawksight-AI/normalize
Normalize Module Enhancements & Comprehensive Testing
2025-12-11 17:00:46 +05:30
Mohd Kaif 1cfbf626d0 Merge pull request #75 from Hawksight-AI/knowledge-engineering
feat: Knowledge Engineering Module Enhancements and Testing
2025-12-11 15:23:54 +05:30
63 changed files with 2885 additions and 1491 deletions
-237
View File
@@ -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.
-33
View File
@@ -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
```
-34
View File
@@ -1,34 +0,0 @@
# Enhanced Export Module Testing, Bug Fixes & Notebook Updates
## 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.
## Key Changes
### 1. Bug Fixes & Logic Improvements
- **`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/`)
- **`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.).
- **`tests/test_export_methods_wrapper.py`**: Added specific tests for convenience wrapper functions in `methods.py`, verifying the fix for schema export.
- **`tests/test_notebook_15_export.py`** & **`tests/test_notebooks_simulation.py`**: Simulation tests that replicate cookbook logic to ensure end-to-end functionality.
### 3. Documentation & Notebook Updates
- **`docs/reference/export.md`** & **`semantica/export/export_usage.md`**: Updated to correctly document `YAMLSchemaExporter.export_ontology_schema` instead of the deprecated `export` method.
- **Cookbooks** (`15_Export.ipynb`, `05_Multi_Format_Export.ipynb`):
- Updated `GraphBuilder.build()` calls to pass combined lists (fixing API mismatch).
- Corrected `YAMLSchemaExporter` usage.
- Fixed `VectorExporter` data preparation.
- Adjusted `CSVExporter` paths.
## Verification
All tests passed successfully:
```bash
$ pytest tests/test_export_module.py tests/test_notebooks_simulation.py tests/test_notebook_15_export.py tests/test_export_methods_wrapper.py
...
13 passed in 3.82s
```
-45
View File
@@ -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
View File
@@ -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.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
-17
View File
@@ -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.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
test content
-34
View File
@@ -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}")
-55
View File
@@ -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()
-179
View File
@@ -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()
-137
View File
@@ -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()
-96
View File
@@ -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()
-194
View File
@@ -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()
-116
View File
@@ -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.
+1 -1
View File
@@ -31,7 +31,7 @@ class LLMOntologyGenerator:
)
base_uri = options.get("base_uri")
name = options.get("name") or "GeneratedOntology"
name = options.get("name")
version = options.get("version") or "1.0"
prompt = self._build_prompt(text=text, name=name, base_uri=base_uri)
+4
View File
@@ -216,6 +216,10 @@ class NamespaceManager:
def _to_camel_case(self, name: str) -> str:
"""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
words = re.findall(r"[a-zA-Z0-9]+", name)
if not words:
+8 -4
View File
@@ -262,8 +262,8 @@ class NamingConventions:
# camelCase for object properties
suggested = self._to_camel_case(name)
else:
# lowercase for data properties
suggested = name.lower()
# camelCase for data properties as well (standard practice)
suggested = self._to_camel_case(name)
return suggested
@@ -364,6 +364,10 @@ class NamingConventions:
def _to_camel_case(self, name: str) -> str:
"""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)
if not words:
return "hasProperty"
@@ -381,8 +385,8 @@ class NamingConventions:
# Basic singularization rules
if name.lower().endswith("ies"):
return name[:-3] + "y"
elif name.lower().endswith("es"):
elif name.lower().endswith("es") and not name.lower().endswith("ss"):
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
+13 -2
View File
@@ -170,7 +170,13 @@ class OntologyGenerator:
self.progress_tracker.update_tracking(
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
self.progress_tracker.update_tracking(
@@ -266,9 +272,14 @@ class OntologyGenerator:
relationships = options.get("relationships", [])
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
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
+5
View File
@@ -89,6 +89,11 @@ class PropertyGenerator:
submodule="PropertyGenerator",
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:
properties = []
+49 -14
View File
@@ -37,6 +37,7 @@ from bs4 import BeautifulSoup
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
@dataclass
@@ -65,6 +66,20 @@ class HTMLElement:
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:
"""HTML document parser."""
@@ -81,7 +96,7 @@ class HTMLParser:
def parse(
self, html_content: Union[str, Path], base_url: Optional[str] = None, **options
) -> Dict[str, Any]:
) -> HTMLData:
"""
Parse HTML content.
@@ -96,7 +111,7 @@ class HTMLParser:
- clean_text: Whether to clean extracted text (default: True)
Returns:
dict: Parsed HTML data
HTMLData: Parsed HTML data
"""
# Track HTML parsing
file_path = None
@@ -160,16 +175,16 @@ class HTMLParser:
status="completed",
message=f"Parsed HTML: {len(links)} links, {len(images)} images",
)
return {
"metadata": metadata.__dict__,
"text": text,
"html": html_string,
"links": links,
"images": images,
"forms": forms,
"tables": tables,
"structure": structure,
}
return HTMLData(
metadata=metadata.__dict__,
text=text,
html=html_string,
links=links,
images=images,
forms=forms,
tables=tables,
structure=structure,
)
except Exception as e:
self.progress_tracker.stop_tracking(
@@ -184,6 +199,26 @@ class HTMLParser:
)
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:
"""
Extract text from HTML.
@@ -203,7 +238,7 @@ class HTMLParser:
extract_tables=False,
clean_text=clean,
)
return result["text"]
return result.text
def extract_links(
self, html_content: Union[str, Path], base_url: Optional[str] = None
@@ -225,7 +260,7 @@ class HTMLParser:
extract_forms=False,
extract_tables=False,
)
return result["links"]
return result.links
def _extract_metadata(self, soup: BeautifulSoup) -> HTMLMetadata:
"""Extract metadata from HTML."""
@@ -58,6 +58,9 @@ class StructuredDataParser:
self.config = config or {}
self.config.update(kwargs)
# Initialize progress tracker
self.progress_tracker = get_progress_tracker()
# Initialize parsers
self.json_parser = JSONParser(**self.config.get("json", {}))
self.csv_parser = CSVParser(**self.config.get("csv", {}))
+53 -3
View File
@@ -34,6 +34,7 @@ License: MIT
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional
import re
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
@@ -203,9 +204,58 @@ class AbductiveReasoner:
def _rule_explains_observation(self, rule: Rule, observation: Observation) -> bool:
"""Check if rule can explain observation."""
# Simple check: rule conclusion matches observation
# Can be enhanced with more sophisticated matching
return True
# Check if rule conclusion matches observation description
# Try exact match first
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:
"""Calculate how well rule covers observation."""
+158 -34
View File
@@ -32,6 +32,7 @@ License: MIT
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Set
import re
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
@@ -148,12 +149,16 @@ class DeductiveReasoner:
rules = self.rule_manager.get_all_rules()
for rule in rules:
# Check if rule can be applied
if self._can_apply_rule(rule, premises):
conclusion = self._apply_rule_to_premises(rule, premises)
# Find all matches (bindings) for the rule
matches = self._find_matches(rule.conditions, {})
for bindings in matches:
conclusion = self._apply_rule_to_premises(rule, premises, bindings)
if conclusion:
conclusions.append(conclusion)
self.known_facts.add(conclusion.statement)
# Check if conclusion is new (not in known facts)
if conclusion.statement not in self.known_facts:
conclusions.append(conclusion)
self.known_facts.add(conclusion.statement)
self.progress_tracker.stop_tracking(
tracking_id,
@@ -168,39 +173,127 @@ class DeductiveReasoner:
)
raise
def _can_apply_rule(self, rule: Rule, premises: List[Premise]) -> bool:
"""Check if rule can be applied to premises."""
# Check if all rule conditions match premises
premise_statements = {p.statement for p in premises}
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
for condition in rule.conditions:
if (
condition not in premise_statements
and condition not in self.known_facts
):
return False
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
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(
self, rule: Rule, premises: List[Premise]
self, rule: Rule, premises: List[Premise], bindings: Dict[str, str]
) -> Optional[Conclusion]:
"""Apply rule to premises and generate conclusion."""
# Find matching premises
matching_premises = [
p
for p in premises
if p.statement in rule.conditions or p.statement in self.known_facts
]
# Find matching premises (those that support the bindings)
# This is a bit approximate, ideally we track which premise supported which condition
matching_premises = []
# 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_id=f"conc_{len(matching_premises)}",
statement=rule.conclusion,
conclusion_id=f"conc_{rule.name}_{len(matching_premises)}",
statement=conclusion_stmt,
premises=matching_premises,
rule_applied=rule,
confidence=rule.confidence,
proof_steps=[f"Applied rule: {rule.name}"],
metadata={"rule_id": rule.rule_id},
proof_steps=[f"Applied rule: {rule.name} with bindings {bindings}"],
metadata={"rule_id": rule.rule_id, "bindings": bindings},
)
return conclusion
@@ -272,6 +365,7 @@ class DeductiveReasoner:
return None
# Check if goal is already known
# Try direct match
if goal in self.known_facts:
return Conclusion(
conclusion_id=f"known_{goal}",
@@ -279,35 +373,65 @@ class DeductiveReasoner:
confidence=1.0,
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
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
premise_conclusions = []
all_proven = True
current_bindings = initial_bindings.copy()
for condition in rule.conditions:
# Instantiate condition with current bindings
instantiated_cond = self._substitute_bindings(condition, current_bindings)
premise_conclusion = self._prove_backward(
condition, proof, depth + 1, max_depth, **options
instantiated_cond, proof, depth + 1, max_depth, **options
)
if 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:
all_proven = False
break
if all_proven:
# All premises proven, rule can fire
# Instantiate conclusion with final bindings
final_conclusion = self._substitute_bindings(rule.conclusion, current_bindings)
conclusion = Conclusion(
conclusion_id=f"conc_{goal}",
statement=goal,
premises=[Premise(p, p) for p in rule.conditions],
statement=final_conclusion,
premises=[p for p in premise_conclusions], # Use actual premises found
rule_applied=rule,
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
+154 -38
View File
@@ -29,6 +29,7 @@ Author: Semantica Contributors
License: MIT
"""
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Set
@@ -194,10 +195,12 @@ class InferenceEngine:
)
for rule in rules:
# Check if rule can fire
if self._can_rule_fire(rule):
# Apply rule
result = self._apply_rule(rule)
# Find all matches for the rule
matches = self._find_matches(rule.conditions, {})
for bindings in matches:
# Apply rule with bindings
result = self._apply_rule(rule, bindings=bindings)
if result:
# Only consider it a new inference if the fact wasn't already known
if self.add_fact(result.conclusion):
@@ -244,47 +247,83 @@ class InferenceEngine:
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:
if goal in self.facts:
is_fact = True
found_fact = goal
except TypeError:
if goal in self.unhashable_facts:
is_fact = True
if is_fact:
found_fact = goal
# 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(
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
self.progress_tracker.update_tracking(
tracking_id, message="Finding rules that can prove the goal..."
)
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(
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
premises = []
for rule, initial_bindings in applicable_rules_and_bindings:
# Try to prove premises with bindings, propagating bindings between premises
current_bindings = initial_bindings.copy()
premises_results = []
all_premises_proven = True
for premise in rule.conditions:
premise_result = self.backward_chain(premise, **options)
for cond in rule.conditions:
# 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:
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:
all_premises_proven = False
break
if all_premises_proven:
# 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:
self.progress_tracker.stop_tracking(
tracking_id,
@@ -304,38 +343,115 @@ class InferenceEngine:
)
raise
def _can_rule_fire(self, rule: Rule) -> bool:
"""Check if rule can fire (all conditions met)."""
for condition in rule.conditions:
try:
if condition not in self.facts:
return False
except TypeError:
if condition not in self.unhashable_facts:
return False
return True
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 _rule_concludes(self, rule: Rule, goal: Any) -> bool:
"""Check if rule concludes the goal."""
return rule.conclusion == goal
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.
"""
# 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(
self, rule: Rule, premises: Optional[List[Any]] = None
self, rule: Rule, premises: Optional[List[Any]] = None, bindings: Optional[Dict[str, str]] = None
) -> Optional[InferenceResult]:
"""Apply rule and return inference result."""
conclusion = rule.conclusion
if bindings:
conclusion = self._substitute_bindings(conclusion, bindings)
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(
conclusion=rule.conclusion,
conclusion=conclusion,
premises=premises,
rule_used=rule,
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
def infer(self, query: Any, **options) -> List[InferenceResult]:
"""
Perform inference based on strategy.
+24 -2
View File
@@ -194,18 +194,21 @@ class SeedDataManager:
entity_type: Optional[str] = None,
relationship_type: Optional[str] = None,
source_name: Optional[str] = None,
delimiter: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
Load seed data from CSV file.
Reads a CSV file and converts rows to dictionaries. Automatically
adds entity_type, relationship_type, and source metadata if provided.
Supports automatic delimiter detection if not provided.
Args:
file_path: Path to CSV file
entity_type: Optional entity type to add to all records
relationship_type: Optional relationship type to add to all records
source_name: Optional source name for tracking
delimiter: Optional CSV delimiter. If None, attempts to detect it.
Returns:
List of loaded data records as dictionaries
@@ -215,7 +218,7 @@ class SeedDataManager:
Example:
>>> 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(
module="seed",
@@ -239,7 +242,21 @@ class SeedDataManager:
tracking_id, message="Reading CSV file..."
)
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:
# Clean up row data
record = {k: v for k, v in row.items() if v}
@@ -316,6 +333,11 @@ class SeedDataManager:
elif "records" in data:
records = data["records"]
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]
else:
records = []
+24 -5
View File
@@ -48,6 +48,17 @@ records = manager.load_from_csv(
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")
# 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"}, ...]
# - Dict with 'entities': {"entities": [...]}
# - 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
@@ -553,11 +568,15 @@ manager.export_seed_data("output/custom_seed.json", format="json")
**Algorithm**: Row-by-row CSV processing with metadata injection
1. **File Reading**: Open CSV file with UTF-8 encoding
2. **Header Detection**: Use csv.DictReader() for automatic header detection
3. **Row Processing**: Iterate through rows, convert to dictionaries
4. **Data Cleaning**: Remove empty values, clean whitespace
5. **Metadata Injection**: Add entity_type, relationship_type, source metadata
6. **Type Conversion**: Convert string values to appropriate types
2. **Delimiter Detection**:
- Use provided delimiter if specified
- If not, attempt to auto-detect delimiter using `csv.Sniffer`
- Fallback to comma (`,`) if detection fails
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
**Space Complexity**: O(n) for records storage
+12 -4
View File
@@ -302,14 +302,22 @@ class OntologyVisualizer:
# Add domain edges
domain = prop.get("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
range_val = prop.get("range")
if range_val:
edges.append(
{"source": prop_name, "target": range_val, "type": "range"}
)
if isinstance(range_val, list):
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(
nodes, edges, output, file_path, **options
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+209
View File
@@ -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()
+162
View File
@@ -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()
+294
View File
@@ -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()
+149
View File
@@ -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()
+486
View File
@@ -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
+152
View File
@@ -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
+154
View File
@@ -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
+289
View File
@@ -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"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.