Compare commits

..
Author SHA1 Message Date
KaifAhmad1 dd0a9d8c90 docs(ontology): explain thresholds as prose instead of a table
A four-row table was heavier than this needed; each threshold's
meaning reads faster as two connected sentences.
2026-09-03 23:35:33 +05:30
KaifAhmad1 044fa2742e docs(ontology): document quality gate threshold semantics
The Ontology Quality Gate section (#1397) showed a thresholds={...}
example but never explained what min_coverage, max_errors,
max_warnings, or fail_on_warnings actually mean, or that
fail_on_warnings is a separate parameter rather than a thresholds
key. Add a concise defaults/semantics table, verified against
OntologyQualityGate.DEFAULT_THRESHOLDS and __init__ in quality_gate.py.
2026-09-03 23:32:05 +05:30
Mohd Kaif 4c6eab632f Merge pull request #1397 from T1mn/codex/ontology-quality-gate
feat(ontology): add a CI-friendly quality gate
2026-09-03 23:20:39 +05:30
Mohd Kaif 103ab04970 Merge branch 'main' into codex/ontology-quality-gate 2026-09-03 23:05:32 +05:30
Zohaib Hassnain 88a57b39f9 docs: replace retired claude-sonnet-4-20250514 model id (#1449)
* docs: replace retired claude-sonnet-4-20250514 model id

* docs(llms): sweep retired model as qodo found
2026-09-03 22:29:50 +05:00
Zohaib HassnainandMohd Kaif f45499b5a7 fix(deps): unblock python 3.9 core install (#1445)
Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com>
2026-09-03 22:51:43 +05:30
Zohaib Hassnain c04adcd1a9 docs(semantic-extraction): fix summary output and model ID (#1448)
* docs(semantic-extraction): fix summary print and the model id

* chore: correct model id

* docs: make triplet valid split meaningful
2026-09-03 22:14:23 +05:00
Zohaib Hassnain a85cf913a5 docs(reasoning): clarify Datalog query result ordering (#1447)
* docs(reasoning): note DatalogReasoner.query() result is not guaranteed

* keep lists[dict] shape
2026-09-03 21:58:39 +05:00
Mohd Kaif 6ba433fea0 docs(index): rewrite landing page as a crisp developer welcome (#1446)
Replace the long feature-dump landing page with a lean "Welcome to
Semantica" page: a two-line problem/positioning statement (deterministic
semantic layer, no LLM required for graph construction, reasoning, or
provenance), five capability bullets, the multi-provider quickstart
snippet, and a 4-step onboarding path. Drops the redundant module
table, industry-use-case grid, and duplicate link lists in favor of
linking out to Core Concepts, guides, and the API reference. Keeps a
collapsed module-list accordion so the page still satisfies
docs_check.py's full-module-coverage check.
2026-09-03 22:26:38 +05:30
Guofang.Tang 75bcb64681 Merge branch 'main' into codex/ontology-quality-gate 2026-09-04 00:34:54 +08:00
Zohaib Hassnain f6a0e4a32e docs(pipeline): wire configured FailureHandler into the engine (#1444) 2026-09-03 21:17:02 +05:00
Mohd Kaif 111bcf997e Revamp badges and add community links in README
Updated badge styles and added community links.
2026-09-03 21:36:23 +05:30
Zohaib Hassnain 6a07ad29be docs(modules): fix code examples to match the current API (#1443)
* docs: rewrite every code example against the actual API

* docs: address Qodo
2026-09-03 21:03:54 +05:00
T1mn 8778e6a837 fix(ontology): address follow-up quality gate findings 2026-09-03 19:57:04 +08:00
Mohd Kaif 01352d5fd5 Merge branch 'main' into codex/ontology-quality-gate 2026-09-03 15:18:43 +05:30
T1mn 6a55b8c3ee fix(ontology): address quality gate review findings 2026-09-03 10:04:46 +08:00
T1mn 471cbe7711 feat(ontology): add deterministic quality gate 2026-09-03 02:00:57 +08:00
19 changed files with 1344 additions and 350 deletions
+7 -1
View File
@@ -28,7 +28,13 @@
[![GitHub Stars](https://img.shields.io/github/stars/semantica-agi/semantica?style=flat-square&color=FFD700&logo=github&logoColor=white&label=Stars)](https://github.com/semantica-agi/semantica) [![GitHub Forks](https://img.shields.io/github/forks/semantica-agi/semantica?style=flat-square&color=6E40C9&logo=github&logoColor=white&label=Forks)](https://github.com/semantica-agi/semantica/network/members) [![Contributors](https://img.shields.io/github/contributors/semantica-agi/semantica?style=flat-square&color=2EA043&logo=github&logoColor=white)](https://github.com/semantica-agi/semantica/graphs/contributors) [![PyPI](https://img.shields.io/pypi/v/semantica.svg?style=flat-square&color=0066CC&logo=pypi&logoColor=white)](https://pypi.org/project/semantica/) [![Total Downloads](https://static.pepy.tech/badge/semantica?style=flat-square)](https://pepy.tech/project/semantica) [![Python 3.8+](https://img.shields.io/badge/python-3.8+-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![CI](https://img.shields.io/github/actions/workflow/status/semantica-agi/semantica/ci.yml?style=flat-square&label=CI)](https://github.com/semantica-agi/semantica/actions) [![Install Matrix](https://img.shields.io/github/actions/workflow/status/semantica-agi/semantica/install-matrix.yml?style=flat-square&label=pip%20install)](https://github.com/semantica-agi/semantica/actions/workflows/install-matrix.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/semantica-agi/semantica/badge?style=flat-square)](https://scorecard.dev/viewer/?uri=github.com/semantica-agi/semantica) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/semantica-agi/semantica)
[![Website](https://img.shields.io/badge/Website-getsemantica.ai-000000?style=flat-square&logo=googlechrome&logoColor=white)](https://getsemantica.ai/) [![Docs](https://img.shields.io/badge/Docs-docs.getsemantica.ai-0099FF?style=flat-square&logo=readthedocs&logoColor=white)](https://docs.getsemantica.ai/) [![Discord](https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/sV34vps5hH) [![Twitter/X](https://img.shields.io/badge/Follow-%40BuildSemantica-000000?style=flat-square&logo=x&logoColor=white)](https://x.com/BuildSemantica) [![YouTube](https://img.shields.io/badge/YouTube-Watch%20Demos-FF0000?style=flat-square&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=QfnNZg4-dZA) [![Changelog](https://img.shields.io/badge/Changelog-View-6E40C9?style=flat-square&logo=keepachangelog&logoColor=white)](CHANGELOG.md)
[![Website](https://img.shields.io/badge/Website-getsemantica.ai-000000?style=for-the-badge\&logo=googlechrome\&logoColor=white)](https://getsemantica.ai/)
[![Docs](https://img.shields.io/badge/Docs-docs.getsemantica.ai-0099FF?style=for-the-badge\&logo=readthedocs\&logoColor=white)](https://docs.getsemantica.ai/)
[![Community](https://img.shields.io/badge/Community-Join%20Discord-5865F2?style=for-the-badge\&logo=discord\&logoColor=white)](https://discord.gg/sV34vps5hH)
[![X](https://img.shields.io/badge/X-%40BuildSemantica-000000?style=for-the-badge\&logo=x\&logoColor=white)](https://x.com/BuildSemantica)
[![YouTube](https://img.shields.io/badge/YouTube-Watch%20Demos-FF0000?style=flat-square\&logo=youtube\&logoColor=white)](https://www.youtube.com/watch?v=QfnNZg4-dZA)
```bash
pip install semantica
+5 -5
View File
@@ -195,7 +195,7 @@ apt29_intel = context.retrieve(
```python
from semantica.llms import LiteLLM
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
result = context.query_with_reasoning(
"What are APT29's known TTPs against healthcare infrastructure, "
@@ -281,7 +281,7 @@ context.store(
link_entities=True,
)
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
result = context.query_with_reasoning(
"Trace the C2 infrastructure chain for APT29 operations targeting "
"ITAR-controlled contractors in 2025. Include IP ranges, ASNs, and TTPs.",
@@ -351,7 +351,7 @@ Parent: wmiprvse.exe
Sigma match: T1053.005 Scheduled Task/Job
"""
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
triage = soc_context.query_with_reasoning(
"Triage this SIEM alert and identify the correct response runbook:\n{}".format(alert_text),
llm_provider=llm,
@@ -425,7 +425,7 @@ Patient: 68F, AF, CKD stage 3b (eGFR 32). On warfarin (INR target 2.03.0).
Presenting for elective hip replacement. Concurrent: amiodarone 200mg, atorvastatin 40mg.
"""
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
answer = clinical_context.query_with_reasoning(
"What is the evidence-based warfarin bridging protocol for this patient "
"given CKD and amiodarone interaction risk?\n\n{}".format(patient_context),
@@ -495,7 +495,7 @@ compliance_context.store(
extract_relationships=True,
)
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
answer = compliance_context.query_with_reasoning(
"Under Basel III CRE20, what are the RWA calculation requirements for "
"commercial real estate exposures with LTV > 80%? "
+8 -8
View File
@@ -275,20 +275,20 @@ print(data)
**LiteLLM** is a universal adapter that provides a single interface to over 100 different LLM providers, including Anthropic Claude, Azure OpenAI, AWS Bedrock, Google Vertex AI, and local Ollama instances. It acts as a translation layer, converting your unified API calls into provider-specific requests, enabling easy switching between providers without code changes.
`LiteLLM` is the Swiss Army knife. It wraps the `litellm` library, which speaks to every major provider using a unified completion API. The model string encodes both provider and model name: `"anthropic/claude-sonnet-4-20250514"`, `"azure/gpt-4o"`, `"bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"`, `"ollama/llama3.2"`. Change the string, change the provider — no other code changes needed.
`LiteLLM` is the Swiss Army knife. It wraps the `litellm` library, which speaks to every major provider using a unified completion API. The model string encodes both provider and model name: `"anthropic/claude-sonnet-5"`, `"azure/gpt-4o"`, `"bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0"`, `"ollama/llama3.2"`. Change the string, change the provider — no other code changes needed.
```python
from semantica.llms import LiteLLM
# Anthropic Claude — highest accuracy for complex reasoning
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
# Reads ANTHROPIC_API_KEY from environment
# Azure OpenAI — compliance and data-residency requirements
llm = LiteLLM(model="azure/gpt-4o", api_key="YOUR_AZURE_KEY")
# AWS Bedrock — existing cloud agreement, no new vendor
llm = LiteLLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
llm = LiteLLM(model="bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0")
# Google Vertex AI
llm = LiteLLM(model="vertex_ai/gemini-1.5-pro")
@@ -306,7 +306,7 @@ The environment-variable convention for each provider: `ANTHROPIC_API_KEY`, `AZU
import os
PROVIDER_MAP = {
"prod": "anthropic/claude-sonnet-4-20250514",
"prod": "anthropic/claude-sonnet-5",
"staging": "openai/gpt-4o-mini",
"local": "ollama/llama3.2",
"azure": "azure/gpt-4o",
@@ -378,7 +378,7 @@ print("FAST: {} (conf={:.0%})".format(fast_result["response"], fast_result["con
# Tier 2: deep answer with Claude if confidence is below threshold
if fast_result["confidence"] < 0.85:
deep_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
deep_llm = LiteLLM(model="anthropic/claude-sonnet-5")
deep_result = context.query_with_reasoning(
query, llm_provider=deep_llm, max_results=15, max_hops=3
)
@@ -574,7 +574,7 @@ print("TRIAGE: {} (conf={:.0%})".format(triage["response"], triage["confidence"]
# Tier 2: escalate to Claude for deep analysis if Tier 1 is uncertain
if triage["confidence"] < 0.88:
deep_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
deep_llm = LiteLLM(model="anthropic/claude-sonnet-5")
deep = context.query_with_reasoning(
"Full MITRE ATT&CK analysis of this alert: identify the attack chain, "
"blast radius, affected systems, and recommended containment steps.",
@@ -630,7 +630,7 @@ for d in drugs:
# trastuzumab (conf=0.98), pertuzumab (conf=0.97), docetaxel (conf=0.96)
# Report synthesis with Claude — switch to azure/gpt-4o for HIPAA by changing one string
report_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
report_llm = LiteLLM(model="anthropic/claude-sonnet-5")
# For HIPAA-constrained Azure deployment:
# report_llm = LiteLLM(model="azure/gpt-4o", api_key="YOUR_AZURE_KEY")
@@ -682,7 +682,7 @@ question = (
# Two-provider consensus — same query, same graph, different LLMs
gpt4o = OpenAI(model="gpt-4o", api_key="YOUR_OAI_KEY")
claude = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
claude = LiteLLM(model="anthropic/claude-sonnet-5")
answer_a = context.query_with_reasoning(question, llm_provider=gpt4o, max_results=10)
answer_b = context.query_with_reasoning(question, llm_provider=claude, max_results=10)
+4 -4
View File
@@ -197,7 +197,7 @@ reasoning_agent.load("./pipeline/enriched_intel/")
# All memories, graph nodes, and vector embeddings from both ingestion agents are now available.
# Use a high-capability model for the synthesis step
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
synthesis = reasoning_agent.query_with_reasoning(
"Summarize the APT29 exploitation of CVE-2024-3400: affected products, "
@@ -428,7 +428,7 @@ tier1.store(
# --- Tier 2: deep investigation when Tier 1 confidence is low ---
if triage["confidence"] < 0.90:
deep_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
deep_llm = LiteLLM(model="anthropic/claude-sonnet-5")
investigation = tier2.query_with_reasoning(
"Full MITRE ATT&CK analysis of incident {}. "
@@ -533,7 +533,7 @@ t1.start(); t2.start()
t1.join(); t2.join()
# Chief agent synthesizes across literature and experimental data
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
synthesis = chief.query_with_reasoning(
"Identify the top two candidate compounds for KRAS G12C NSCLC that show "
@@ -576,7 +576,7 @@ credit_officer = make_desk_agent()
committee_chair = make_desk_agent()
app_id = "LOAN-2025-88421"
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
llm = LiteLLM(model="anthropic/claude-sonnet-5")
# --- Risk Desk: PD/LGD/EL analysis ---
risk_desk.store(
+1 -1
View File
@@ -477,7 +477,7 @@ regs = [
]
# Use an LLM to extract the conceptual model from regulatory prose
llm_gen = LLMOntologyGenerator(provider="anthropic", model="claude-sonnet-4-20250514")
llm_gen = LLMOntologyGenerator(provider="anthropic", model="claude-sonnet-5")
ontology = llm_gen.generate_ontology_from_text(
"\n\n".join(r.text[:8000] for r in regs) # token-safe excerpt per document
)
+4 -2
View File
@@ -127,7 +127,7 @@ engine = ExecutionEngine(max_workers=4, retry_on_failure=True)
result = engine.execute_pipeline(pipeline)
print(f"Success: {result.success}")
print(f"Output: {result.output}") # {"node_count": 312, "edge_count": 847}
print(f"Output: {result.output}") # the final step's return value, e.g. {"node_count": ..., "edge_count": ...}
print(f"Duration: {result.metrics['execution_time']:.2f}s")
print(f"Steps completed: {result.metrics['steps_executed']}")
```
@@ -197,7 +197,9 @@ engine = ExecutionEngine(
max_workers = 4,
retry_on_failure = True,
)
# The engine uses handler.get_retry_policy(step.step_type) when a step fails
# ExecutionEngine builds its own FailureHandler; replace it with the configured one
engine.failure_handler = handler
# The engine now calls engine.failure_handler.get_retry_policy(step.step_type) on failure
```
`handler.classify_error()` distinguishes `ValidationError` (low severity, usually don't retry), `ProcessingError` (high severity), and timeout/connection errors (medium severity, always retry). You can inspect the classification:
+21 -16
View File
@@ -100,14 +100,15 @@ ner = NamedEntityRecognizer(
methods=["llm", "ml", "pattern"],
confidence_threshold=0.75,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
entities = ner.extract_entities(report)
for e in entities:
print("[{:>5.2f}] {:15s} {}".format(e.confidence, e.label, e.text))
# Expected output (abbreviated):
# Illustrative output — exact labels and scores depend on the method and model.
# Abbreviated:
# [ 0.94] THREAT_ACTOR GAMMA-7
# [ 0.91] THREAT_ACTOR DELTA-3
# [ 0.97] MALWARE HAMMERTOSS
@@ -262,16 +263,18 @@ from semantica.semantic_extract import TripletExtractor
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
include_temporal=True, # attach time context to triplets when available
include_provenance=True, # embed source document reference in each triplet
validate=False, # return raw triplets; validate explicitly below
)
# Feed in the entities and relations you already extracted — the extractor
# uses them to constrain and validate what it produces
# uses them to constrain what it produces
triplets = tri.extract_triplets(report, entities, relations)
# Filter malformed triplets before serialisation
# (extract_triplets validates automatically unless validate=False, as above)
valid = tri.validate_triplets(triplets)
print("Valid: {}/{}".format(len(valid), len(triplets)))
@@ -320,7 +323,7 @@ def ingest_intel_report(
methods=[method, "pattern"],
confidence_threshold=0.70,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
entities = ner.extract_entities(text)
classified = ner.classify_entities(entities)
@@ -335,7 +338,7 @@ def ingest_intel_report(
relation_types=["deployed", "targets", "exploits", "operates_from", "provided_to"],
confidence_threshold=0.65,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
relations = rel.extract_relations(text, entities)
@@ -347,9 +350,10 @@ def ingest_intel_report(
tri = TripletExtractor(
method=method,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
include_temporal=True,
include_provenance=True,
validate=False, # keep raw triplets so the summary can report rejections
)
triplets = tri.extract_triplets(text, entities, relations)
valid = tri.validate_triplets(triplets)
@@ -377,6 +381,7 @@ def ingest_intel_report(
"coref_chains": len(chains),
"relations": len(relations),
"events": len(events),
"triplets_total": len(triplets),
"triplets_valid": len(valid),
"graph_nodes": graph_stats.get("graph_nodes", 0),
"graph_edges": graph_stats.get("graph_edges", 0),
@@ -402,7 +407,7 @@ for text, doc_id in reports:
summary["relations"],
summary["events"],
summary["triplets_valid"],
len(summary["rdf_turtle"]),
summary["triplets_total"],
))
```
@@ -421,7 +426,7 @@ ner = NamedEntityRecognizer(
methods=["llm", "pattern"],
confidence_threshold=0.75,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
entities = ner.extract_entities(fintel_text)
grouped = ner.classify_entities(entities)
@@ -438,14 +443,14 @@ rel = RelationExtractor(
relation_types=["operates_from", "deployed", "targets", "exploits"],
confidence_threshold=0.70,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
relations = rel.extract_relations(fintel_text, entities)
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
include_temporal=True,
include_provenance=True,
)
@@ -544,14 +549,14 @@ rel = RelationExtractor(
relation_types=["treats", "causes_adverse_event", "has_efficacy", "evaluated_in"],
confidence_threshold=0.65,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
relations = rel.extract_relations(paper, entities)
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
triplet_types=["treats", "has_efficacy", "causes_adverse_event"],
include_temporal=True,
include_provenance=True,
@@ -595,7 +600,7 @@ ner = NamedEntityRecognizer(
methods=["llm", "ml", "pattern"],
confidence_threshold=0.70,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
entities = ner.extract_entities(credit_memo)
grouped = ner.classify_entities(entities)
@@ -612,14 +617,14 @@ rel = RelationExtractor(
relation_types=["guaranteed_by", "secured_by", "classified_as", "exposed_to"],
confidence_threshold=0.65,
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
)
relations = rel.extract_relations(credit_memo, entities)
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-4-6",
llm_model="claude-sonnet-5",
include_temporal=True,
include_provenance=True,
)
+24 -293
View File
@@ -1,5 +1,5 @@
---
title: "Semantica"
title: "Welcome to Semantica"
description: "The Context and Semantic Layer for AI in High-Stakes Domains: Context Graphs · Decision Intelligence · Full Provenance"
---
@@ -7,93 +7,25 @@ description: "The Context and Semantic Layer for AI in High-Stakes Domains: Cont
pip install semantica
```
Most AI agents store embeddings, not meaning. They can't say why a fact was recalled, where it came from, or what led to a decision. In healthcare, finance, legal, and government, that lack of a traceable record blocks production deployment.
Most AI agents run on embeddings, not meaning. A similarity score has no structure, no relationships, and no way to explain why a result came back.
Semantica is the context and semantic layer for AI in high-stakes domains, sitting beneath your existing agent framework. It doesn't replace LangChain or LlamaIndex; it makes their outputs traceable.
Semantica is the semantic and context layer underneath your LLM, vector store, and agent framework: deterministic infrastructure, not a model. Graph construction, reasoning, and provenance all run without an LLM in the loop. It turns fragmented enterprise data into a structured, queryable context graph and knowledge graph, governed by ontologies, taxonomies, and controlled vocabularies (OWL, SHACL, SKOS), so your data's meaning is explicit rather than approximated by an embedding.
Provenance and audit trails aren't a bolt-on. They fall out naturally once your data has that structure, so the same graph that powers retrieval and reasoning also gives you a straight answer when a regulator asks why.
## What Most AI Stacks Are Missing
## What you get
**No memory structure.** Agents store embeddings, not meaning.
- No way to ask *why* a fact was recalled
- No link from a recalled fact back to its source document
- Context is a black box that resets on every run
**No decision trail.** Agents act continuously but record nothing.
- No history to hand to a regulator or auditor
- No way to replay or reproduce a past decision
- Debugging means re-running, not reviewing
**No provenance.** Outputs can't be traced to source facts.
- A hard compliance blocker in healthcare, finance, and legal
- No lineage from inference back to the original document
- No way to demonstrate what the agent actually relied on
**No reasoning transparency.** Black-box answers with no explanation.
- No way to validate the reasoning path
- No way to contest a specific conclusion
- No basis for improving or correcting future behavior
**No conflict detection.** Contradictory facts silently coexist in vector stores.
- No detection when two sources disagree
- Outputs become inconsistent and unpredictable over time
- Silent failures compound as the knowledge base grows
## What Semantica Adds to Your Stack
Semantica gives every agent the infrastructure it needs to be accountable, and it drops into an existing setup in minutes.
**Context Graphs.** A structured, queryable graph of everything your agent knows, decides, and reasons about.
- Persistent across agent runs, with no context loss between sessions
- Queryable with SPARQL and full graph algorithms
- Temporal model with `valid_from` / `valid_until` on nodes and edges
- Point-in-time snapshots of the full knowledge state
**Decision Intelligence.** Every decision is a first-class object in your system.
- `record_decision()` captures full lifecycle and causal chain
- Hybrid precedent search over past decisions for consistency
- `analyze_decision_impact()` shows downstream consequences
- Causal chain visualization from trigger to outcome
**Full Provenance.** Every fact links to its source document and ingestion event.
- W3C PROV-O compliant lineage across all modules
- Full traceability from raw input to final inference
- `recorded_at` stamping with OWL-Time export
- Audit-ready for HIPAA, SOX, GDPR, FDA 21 CFR Part 11
**Reasoning Engines.** Explainable reasoning paths, not black boxes.
- Forward chaining, Rete, deductive, abductive
- SPARQL query-based inference over RDF graphs
- Datalog with recursive Horn clause rules
- Every conclusion backed by a traceable derivation path
**Temporal Intelligence.** Your graph knows not just *what*, but *when*.
- Allen interval algebra covering all 13 temporal relations
- Point-in-time queries over historical graph states
- Temporal provenance stamping on every fact
- OWL-Time export for standards-compliant archiving
**Ontology Hub.** Full ontology lifecycle in the browser.
- Visual editor for schema design and editing
- SHACL Studio for constraint authoring and validation
- Alignment authoring across multiple ontologies
- Health dashboard and version control built in
- **[Context graphs](/guides/context-graphs)**: a persistent, queryable graph of everything your agent knows, decides, and reasons about
- **Decision intelligence**: `record_decision()` captures the full lifecycle and causal chain of every decision
- **[Full provenance](/guides/provenance)**: every fact links back to its source, W3C PROV-O compliant and audit-ready for HIPAA, SOX, and GDPR
- **[Explainable reasoning](/guides/reasoning)**: forward chaining, Datalog, and SPARQL, each with a derivation path you can inspect
- **Temporal intelligence**: Allen interval algebra and point-in-time snapshots, so the graph knows not just *what* but *when*
<Tip>
Works alongside any LLM provider and any agent framework. Add it to an existing stack without changing your architecture.
Works alongside any LLM provider and any agent framework, and ingests directly from enterprise data platforms like Databricks, SAP, Salesforce, and Snowflake. Add it to an existing stack without changing your architecture.
</Tip>
<img src="/assets/img/diagrams/architecture-overview.svg" alt="Semantica four-layer architecture: Ingestion → Processing → Intelligence → Application" style={{ width: '100%', borderRadius: '12px', margin: '24px 0' }} />
## See It In Action
One pip install. A few lines to connect your agent. Everything else becomes traceable.
```bash
pip install semantica
```
## Try it
<CodeGroup>
@@ -175,229 +107,28 @@ decision_id = context.record_decision(
</CodeGroup>
- [Full Quickstart](/quickstart): step-by-step pipeline walkthrough
- [Cookbook](/cookbook): 40+ real-world Jupyter notebooks
- [Join Discord](https://discord.gg/sV34vps5hH): community chat and support
## Industry Use Cases
Semantica is used in domains where every decision must be explainable and every fact must be traceable.
<Warning>
**This is system-level explainability, not foundation-model explainability.** Semantica does not expose, reconstruct, or explain what happens *inside* the LLM/foundation model. Its internal reasoning or chain-of-thought stays opaque, as it does for any external system. What Semantica explains is *outside* the model: the context and data fed in, the decision produced, its provenance, the relevant relationships, the policies applied, and the full execution trail. See [Core Concepts](/concepts) for the full scope note.
</Warning>
**Healthcare & Life Sciences**
- Clinical decision support with full audit trails
- Drug interaction and contraindication graphs
- Patient safety event tracking and root-cause analysis
- HIPAA-compliant provenance chains out of the box
**Finance & Risk**
- Fraud detection knowledge graphs
- Risk assessment trails built to survive an audit
- SOX, GDPR, and MiFID II compliance infrastructure
- Model decision lineage for regulatory reporting
**Legal & Compliance**
- Evidence-backed research with every cited fact provenance-linked
- Contract analysis with traceable clause extraction
- Regulatory change tracking across jurisdictions
- Full reasoning paths ready for court-admissible documentation
**Cybersecurity**
- Threat attribution graphs linking actors, TTPs, and indicators
- Incident response timelines with full event provenance
- Security audit trails across the complete kill chain
- MITRE ATT&CK-aligned knowledge graph integration
**Government & Defense**
- Policy decision trails from brief to outcome
- Classified information handling with provenance chains
- Chain-of-custody scrutiny for intelligence reporting
- Air-gapped deployment with local LLM support
**Critical Infrastructure**
- Power grid state tracking with temporal intelligence
- Transportation safety event graphs
- Emergency response coordination with decision audit trails
- Consequence modeling for high-stakes operational decisions
## Start Here
## Start here
<Steps>
<Step title="Install Semantica">
<Step title="Install">
```bash
pip install semantica
```
See [Installation](/installation) for optional extras (`[all]`, `[neo4j]`, `[pinecone]`) and environment setup.
Optional extras: `[all]`, `[neo4j]`, `[pinecone]`. See [Installation](/installation).
</Step>
<Step title="Run the Quickstart">
Build a complete knowledge graph pipeline in [5 minutes](/quickstart):
- Ingest documents from any source
- Extract entities and relationships
- Build and query the graph
- Record and trace a decision
<Step title="Build a pipeline">
Follow the [Quickstart](/quickstart) to ingest documents, extract entities, build a graph, and record a decision in 5 minutes.
</Step>
<Step title="Learn the mental model">
[Core Concepts](/concepts) covers:
- Knowledge graphs vs. vector stores: when to use each
- What GraphRAG is and how Semantica implements it
- How provenance and decision tracking work together
- The context and semantic layer architecture
<Step title="Learn the model">
[Core Concepts](/concepts) covers knowledge graphs vs. vector stores, GraphRAG, and how provenance and decisions fit together.
</Step>
<Step title="Go deep on any module">
Every module has a dedicated [reference page](/reference/context) with:
- Full class and method documentation
- Parameter tables with types and defaults
- Runnable code examples for each feature
<Step title="Go deep">
Every module has a [reference page](/reference/context) with full API docs and runnable examples.
</Step>
</Steps>
- [Installation](/installation): get Semantica installed in under a minute
- [Quickstart](/quickstart): build a complete knowledge graph pipeline in 5 minutes
- [Core Concepts](/concepts): the mental model behind the API
- [API Reference](/reference/context): exact module, class, and method details
- [Cookbook](/cookbook): domain notebooks for real-world use cases
- [Changelog](https://github.com/semantica-agi/semantica/releases): release history
## Full Capabilities
<AccordionGroup>
<Accordion title="Context & Decision Intelligence" icon="brain">
### Context Graphs
- Structured, persistent graph of entities, relationships, and decisions
- Temporal model with `valid_from` / `valid_until` on every node and edge
- Point-in-time queries across historical graph states
- Distance Intelligence: semantic neighborhoods and N×N distance matrices
### Decision Tracking
- `record_decision()` with full lifecycle management and causal chains
- Hybrid similarity search over past decisions for consistency enforcement
- `analyze_decision_impact()` and `analyze_decision_influence()` for consequence modeling
- Ego-mode exploration for targeted neighborhood investigation
More: the [Cookbook](/cookbook) for real-world notebooks, [Discord](https://discord.gg/sV34vps5hH) for help.
<Accordion title="Full module list">
`semantica.ingest`, `semantica.parse`, `semantica.split`, `semantica.normalize`, `semantica.semantic_extract`, `semantica.kg`, `semantica.ontology`, `semantica.reasoning`, `semantica.embeddings`, `semantica.vector_store`, `semantica.graph_store`, `semantica.triplet_store`, `semantica.context`, `semantica.provenance`, `semantica.change_management`, `semantica.deduplication`, `semantica.conflicts`, `semantica.export`, `semantica.visualization`, `semantica.pipeline`, `semantica.seed`, `semantica.llms`, `semantica.mcp_server`, `semantica.explorer`, `semantica.evals`, `semantica.utils`, `semantica.core`. See the [API Reference](/reference/context) for full docs on each.
</Accordion>
<Accordion title="Knowledge Engineering" icon="diagram-project">
### Entity & Relation Extraction
- Named entity recognition: pattern, ML, or LLM methods
- Typed triplet extraction via LLM or rule-based pipelines
- Event extraction with temporal and causal linking
### Ontology & Schema
- Ontology Hub: visual editor, SHACL Studio, alignments, health dashboard
- Deduplication v2: `blocking_v2`, `hybrid_v2`, `semantic_v2`: up to 7x faster
- Datalog reasoning: recursive Horn clause rules with fixpoint semantics
- SPARQL reasoning: query-based inference over RDF graphs
</Accordion>
<Accordion title="Provenance & Auditability" icon="shield-check">
### Lineage Tracking
- W3C PROV-O lineage across all modules: every fact has a source
- `recorded_at` stamping with full OWL-Time export
- Change management with SHA-256 checksums and version control
- Full audit trails from ingestion event to final inference
### Compliance Infrastructure
- HIPAA: patient data handling with audit-ready provenance chains
- SOX / MiFID II: financial decision records with full traceability
- GDPR: data lineage for subject access and right-to-erasure workflows
- FDA 21 CFR Part 11: electronic records and signature compliance
</Accordion>
<Accordion title="Data Ingestion & Export" icon="database">
### Ingestion Formats
- Documents: PDF, DOCX, HTML, PPTX, Docling layout analysis
- Structured data: JSON, CSV, Excel, Parquet, XML
- Sources: web crawl, SQL, Snowflake, feeds, email, code repositories, MCP
### Vector Stores
- FAISS, Pinecone, Weaviate, Qdrant, Milvus, PgVector, in-memory
### Graph Stores
- Neo4j, FalkorDB, Apache AGE, Amazon Neptune
### Export Formats
- RDF: Turtle, JSON-LD, N-Triples, RDF/XML
- Tabular: Parquet, CSV, Arrow
- Graph: GraphML, GEXF, DOT, ArangoDB AQL
- Ontology: OWL, SKOS, SHACL
</Accordion>
</AccordionGroup>
## Module Reference
| Module | What it provides |
| :-------- | :----------------- |
| `semantica.context` | Context graphs, agent memory, decision tracking, causal analysis, precedent search |
| `semantica.kg` | KG construction, graph algorithms, temporal model, Allen interval algebra |
| `semantica.semantic_extract` | NER, relation extraction, event extraction, triplet generation |
| `semantica.reasoning` | Forward chaining, Rete, deductive, abductive, SPARQL, Datalog |
| `semantica.ontology` | SHACL, SKOS, alignments, diff/migration, auto-generation, OWL/RDF |
| `semantica.explorer` | FastAPI Knowledge Explorer, Ontology Hub, Distance Intelligence, SHACL Studio |
| `semantica.mcp_server` | MCP stdio server: 15 tools for Claude Desktop, VS Code, Cursor, Windsurf, Cline |
| `semantica.vector_store` | FAISS, Pinecone, Weaviate, Qdrant, Milvus, PgVector |
| `semantica.graph_store` | Neo4j, FalkorDB, Apache AGE, Amazon Neptune |
| `semantica.triplet_store` | In-memory and persistent RDF triple store with SPARQL |
| `semantica.ingest` | Files, web, feeds, databases, Snowflake, Parquet, XML, MCP |
| `semantica.parse` | Document parsing: PDF, DOCX, HTML, PPTX, Docling layout analysis |
| `semantica.split` | Text chunking: sentence, paragraph, token, semantic boundary strategies |
| `semantica.normalize` | Text normalization, entity canonicalization, whitespace and encoding cleanup |
| `semantica.embeddings` | Sentence-Transformers, FastEmbed, OpenAI, BGE, Ollama local embeddings |
| `semantica.pipeline` | Pipeline DSL, parallel workers, retry policies, failure handling |
| `semantica.export` | RDF, Parquet, ArangoDB AQL, CSV, OWL, Arrow, GraphML, GEXF, DOT |
| `semantica.visualization` | Programmatic graph rendering: force, hierarchical, circular, spring layouts |
| `semantica.deduplication` | Entity deduplication v1/v2, similarity scoring, blocking, merging |
| `semantica.conflicts` | Conflict detection and resolution across overlapping knowledge sources |
| `semantica.provenance` | W3C PROV-O lineage tracking, source attribution, audit trails |
| `semantica.change_management` | Version control with SHA-256 checksums, diff, rollback |
| `semantica.llms` | Groq, OpenAI, Anthropic, Gemini, Ollama, DeepSeek, Novita AI, LiteLLM, HuggingFace |
| `semantica.seed` | Foundation graph seeding from CSV, JSON, SQL, API, and RDF sources |
| `semantica.evals` | Evaluation harness: KG quality, extraction F1, pipeline benchmarking, regression tracking |
| `semantica.core` | Orchestration, ConfigManager, LifecycleManager, PluginRegistry, MethodRegistry |
| `semantica.utils` | Logging, validation, progress tracking, hash utilities, nested dict helpers |
## Why Semantica?
**Open Source, MIT.** No vendor lock-in, no paywalled features.
- Full source available on GitHub
- Every line auditable by your security team
- Fork, extend, and self-host with no restrictions
- No telemetry, no usage reporting
**Production Ready.** Built for teams that can't afford surprises.
- 1,000+ passing tests with full regression coverage
- `PipelineValidator` catches configuration errors at startup
- `FailureHandler` with exponential backoff and dead-letter queues
- Ongoing security hardening, with fixes shipped in every release ([CHANGELOG](https://github.com/semantica-agi/semantica/blob/main/CHANGELOG.md))
**Modular by Design.** Import only what you need.
- Use `NERExtractor` without a graph store
- Use `ContextGraph` without vector storage
- Every component independently swappable and testable
- No framework lock-in, and works with any agent stack
+7 -7
View File
@@ -129,7 +129,7 @@ from semantica.llms import Groq, OpenAI, LiteLLM, HuggingFaceLLM
from semantica.llms import LiteLLM
llm = LiteLLM(
model="anthropic/claude-sonnet-4-20250514",
model="anthropic/claude-sonnet-5",
api_key=os.getenv("ANTHROPIC_API_KEY"),
temperature=0.0,
)
@@ -198,7 +198,7 @@ llm = Groq(api_key=os.getenv("GROQ_API_KEY"), model="llama-3.1-8b-instant")
# Method 3: Multiple providers via LiteLLM
providers = {
"fast": LiteLLM(model="groq/llama-3.1-8b-instant", api_key=os.getenv("GROQ_API_KEY")),
"smart": LiteLLM(model="anthropic/claude-sonnet-4-20250514", api_key=os.getenv("ANTHROPIC_API_KEY"))
"smart": LiteLLM(model="anthropic/claude-sonnet-5", api_key=os.getenv("ANTHROPIC_API_KEY"))
}
```
@@ -252,7 +252,7 @@ from semantica.llms import LiteLLM
# pip install "semantica[llm-litellm]"
# Anthropic Claude
llm = LiteLLM(model="anthropic/claude-opus-4-5", api_key=os.getenv("ANTHROPIC_API_KEY"))
llm = LiteLLM(model="anthropic/claude-opus-4-7", api_key=os.getenv("ANTHROPIC_API_KEY"))
# Google Gemini
llm = LiteLLM(model="gemini/gemini-1.5-pro", api_key=os.getenv("GOOGLE_API_KEY"))
@@ -267,7 +267,7 @@ llm = LiteLLM(model="deepseek/deepseek-chat", api_key=os.getenv("DEEP
llm = LiteLLM(model="azure/gpt-4o", api_key=os.getenv("AZURE_API_KEY"))
# AWS Bedrock
llm = LiteLLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
llm = LiteLLM(model="bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0")
# Novita AI
llm = LiteLLM(model="novita/deepseek/deepseek-v3.2", api_key=os.getenv("NOVITA_API_KEY"))
@@ -297,12 +297,12 @@ from semantica.llms import LiteLLM
# Pattern: LiteLLM(model="<provider>/<model-name>")
providers = {
"Anthropic": LiteLLM(model="anthropic/claude-opus-4-5", api_key=os.getenv("ANTHROPIC_API_KEY")),
"Anthropic": LiteLLM(model="anthropic/claude-opus-4-7", api_key=os.getenv("ANTHROPIC_API_KEY")),
"Gemini": LiteLLM(model="gemini/gemini-1.5-pro", api_key=os.getenv("GOOGLE_API_KEY")),
"Ollama": LiteLLM(model="ollama/llama3.2:3b", api_base="http://localhost:11434"),
"DeepSeek": LiteLLM(model="deepseek/deepseek-chat", api_key=os.getenv("DEEPSEEK_API_KEY")),
"Azure": LiteLLM(model="azure/gpt-4o", api_key=os.getenv("AZURE_API_KEY")),
"Bedrock": LiteLLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"),
"Bedrock": LiteLLM(model="bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0"),
"Cohere": LiteLLM(model="cohere/command-r-plus", api_key=os.getenv("COHERE_API_KEY")),
"Novita AI": LiteLLM(model="novita/deepseek/deepseek-v3.2", api_key=os.getenv("NOVITA_API_KEY")),
}
@@ -416,7 +416,7 @@ for text in texts:
| :---------- | :--------------------------- | :----------- |
| **Entity Extraction** | `Groq("llama-3.3-70b-versatile")` | Fast, good accuracy for structured tasks |
| **Relation Extraction** | `OpenAI("gpt-4o")` | Best at complex relationship reasoning |
| **Complex Analysis** | `LiteLLM("anthropic/claude-sonnet-4-20250514")` | Highest reasoning capability |
| **Complex Analysis** | `LiteLLM("anthropic/claude-sonnet-5")` | Highest reasoning capability |
| **High Volume/Cost** | `LiteLLM("deepseek/deepseek-chat")` | Lowest cost per token |
### Error Handling
+30
View File
@@ -22,6 +22,7 @@ icon: "sitemap"
| `LLMOntologyGenerator` | LLM-powered ontology generation for complex domains |
| `SHACLGenerator` | Generate SHACL shapes from an ontology or KG schema |
| `OntologyValidator` | Validate any graph against SHACL shapes: returns `SHACLValidationReport` |
| `OntologyQualityGate` | Run deterministic ontology/KG quality checks for CI |
| `OWLGenerator` | Serialize ontologies to Turtle, RDF/XML, JSON-LD |
| `NamespaceManager` | IRI generation, prefix management, and namespace binding |
| `OntologyEvaluator` | Coverage, completeness, and granularity quality metrics |
@@ -81,9 +82,38 @@ engine.export_owl(ontology, "ontology.ttl", format="turtle")
| :------ | :----------- |
| `from_data(data)` | Run the 5-stage pipeline on entity/relationship data |
| `validate_graph(kg, ontology=...)` | Check a knowledge graph against generated SHACL shapes |
| `quality_check(ontology, graph_data=...)` | Return a deterministic quality report and CI-friendly pass/fail result |
| `export_owl(ontology, path, format)` | Serialize to `"turtle"`, `"xml"`, or `"json-ld"` |
| `evaluate(ontology, kg)` | Compute coverage, completeness, and granularity metrics |
### Ontology Quality Gate
Use the quality gate before export or deployment to catch structural issues
without adding a runtime dependency:
```python
from semantica.ontology import ontology_quality_check
report = ontology_quality_check(
ontology,
graph_data=kg,
thresholds={"min_coverage": 0.8},
)
if not report.passed:
for issue in report.issues:
print(issue.code, issue.message)
```
The report checks class/property coverage, orphan schema elements, domain and
range references, and unresolved KG relationship endpoints. It includes
machine-readable issue codes, severity, counts, metrics, and threshold
failures. The first version reports findings only; it does not auto-fix data.
### Thresholds
`min_coverage` (default `0.0`) sets the minimum required `coverage` score, the average of class and property coverage from `0.0` to `1.0`; the gate fails below it. `max_errors` (default `0.0`) caps how many `error`/`critical` issues are allowed before the gate fails. `max_warnings` (default `None`) caps `warning` issues the same way, and `None` means warnings alone never fail the gate. `fail_on_warnings` is a separate parameter, not a `thresholds` key, passed to `OntologyQualityGate(...)` or `.check(...)` directly; when `True`, a single warning fails the gate regardless of `max_warnings`.
## OntologyGenerator (5-Stage Pipeline)
**`OntologyGenerator`** auto-generates a formal ontology from your knowledge graph entities and relationships:
+1 -1
View File
@@ -323,7 +323,7 @@ all_facts = datalog.derive_all()
# Query with variable pattern: variables start with uppercase or ?
results = datalog.query("ancestor(alice, ?Z)")
# → [{"Z": "bob"}, {"Z": "charlie"}, {"Z": "dave"}]
# → a list of binding dicts: [{"Z": "bob"}, {"Z": "charlie"}, {"Z": "dave"}] (order not guaranteed)
# Clear and start over
datalog.clear()
+36 -7
View File
@@ -47,7 +47,11 @@ dependencies = [
"numpy>=2.0.2",
"pandas>=1.3.0",
"scipy>=1.13.1",
"scikit-learn>=1.7.2",
# scikit-learn dropped Python 3.9 support at 1.7.0 (requires_python >=3.10),
# so an unqualified >=1.7.2 floor is unsatisfiable on 3.9. Cap 3.9 to the
# last 3.9-compatible release line; 3.10+ is left unconstrained.
"scikit-learn>=1.6.1,<1.7.0; python_version < '3.10'",
"scikit-learn>=1.7.2; python_version >= '3.10'",
"umap-learn>=0.5.12",
# thinc (spacy's core dep) dropped Python 3.9 wheels at 8.3.10, and later
# spacy patch releases (3.8.8+) require thinc>=8.3.9-only-on-3.10+ ranges,
@@ -66,24 +70,49 @@ dependencies = [
"seaborn>=0.13.2",
"plotly>=6.8.0",
"ipywidgets>=8.0.0",
"requests>=2.34.2",
# requests dropped Python 3.9 support at 2.33.0 (requires_python >=3.10),
# so an unqualified >=2.34.2 floor is unsatisfiable on 3.9. Cap 3.9 to the
# last 3.9-compatible release; 3.10+ is left unconstrained.
"requests>=2.32.5,<2.33.0; python_version < '3.10'",
"requests>=2.34.2; python_version >= '3.10'",
"GitPython>=3.1.58",
"chardet>=7.4.3",
# chardet dropped Python 3.9 support at 6.0.0 (requires_python >=3.10), so
# an unqualified >=7.4.3 floor is unsatisfiable on 3.9. Cap 3.9 to the last
# 3.9-compatible release; 3.10+ is left unconstrained.
"chardet>=5.2.0,<6.0.0; python_version < '3.10'",
"chardet>=7.4.3; python_version >= '3.10'",
"protobuf>=5.29.1,<8.0",
"grpcio>=1.81.1",
# grpcio dropped Python 3.9 support at 1.81.0 (requires_python >=3.10), so
# an unqualified >=1.81.1 floor is unsatisfiable on 3.9. Cap 3.9 to the last
# 3.9-compatible release; 3.10+ is left unconstrained.
"grpcio>=1.80.0,<1.81.0; python_version < '3.10'",
"grpcio>=1.81.1; python_version >= '3.10'",
"beautifulsoup4>=4.15.0",
"lxml>=6.1.1",
"python-docx>=1.2.0",
"openpyxl>=3.1.5",
"pillow>=12.2.0",
# pillow dropped Python 3.9 support at 12.0.0 (requires_python >=3.10), so
# an unqualified >=12.2.0 floor is unsatisfiable on 3.9. Cap 3.9 to the last
# 3.9-compatible release; 3.10+ is left unconstrained.
"pillow>=11.3.0,<12.0.0; python_version < '3.10'",
"pillow>=12.2.0; python_version >= '3.10'",
"librosa>=0.9.0",
"opencv-python>=4.13.0.92",
"faiss-cpu>=1.7.0",
"fastembed>=0.2.0",
"onnxruntime>=1.20.1",
# onnxruntime stopped shipping cp39 wheels at 1.20.0 (its PyPI metadata
# still claims requires_python >=3.9, but no matching wheel exists), so an
# unqualified >=1.20.1 floor is unsatisfiable on 3.9. Cap 3.9 to the last
# release with a cp39 wheel; 3.10+ is left unconstrained.
"onnxruntime>=1.19.2,<1.20.0; python_version < '3.10'",
"onnxruntime>=1.20.1; python_version >= '3.10'",
"tokenizers>=0.15.0",
"pydantic>=2.13.4",
"click>=8.4.2",
# click dropped Python 3.9 support at 8.2.0 (requires_python >=3.10), so an
# unqualified >=8.4.2 floor is unsatisfiable on 3.9. Cap 3.9 to the last
# 3.9-compatible release; 3.10+ is left unconstrained.
"click>=8.1.8,<8.2.0; python_version < '3.10'",
"click>=8.4.2; python_version >= '3.10'",
"rich>=12.5.0",
"tqdm>=4.68.3",
"pyyaml>=6.0",
+4 -1
View File
@@ -301,7 +301,10 @@ class GraphValidator:
code="ORPHAN_NODES",
message=f"Found {len(isolates)} orphan nodes (no relationships).",
severity=ValidationSeverity.WARNING,
details={"count": len(isolates), "ids": isolates[:10]} # Limit output
details={
"count": len(isolates),
"ids": sorted(isolates, key=str)[:10],
} # Limit output deterministically
))
except Exception as e:
+1 -1
View File
@@ -35,7 +35,7 @@ Example Usage:
>>> llm = LiteLLM(model="openai/gpt-4o", api_key="your-key")
>>> response = llm.generate("Hello, world!")
>>> # Or use other providers via LiteLLM
>>> llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
>>> llm = LiteLLM(model="anthropic/claude-sonnet-5")
>>> response = llm.generate("Hello, world!")
>>>
>>> # Anthropic provider
+3 -3
View File
@@ -31,7 +31,7 @@ class LiteLLM:
Provides unified interface to 100+ LLM providers through LiteLLM library.
Supports providers like OpenAI, Anthropic, Groq, Azure, Bedrock, Vertex AI, etc.
Model format: "provider/model-name" (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514", "groq/llama-3.1-8b-instant")
Model format: "provider/model-name" (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-5", "groq/llama-3.1-8b-instant")
Example:
>>> from semantica.llms import LiteLLM
@@ -39,7 +39,7 @@ class LiteLLM:
>>> response = llm.generate("What is AI?")
>>>
>>> # Use with different providers
>>> llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
>>> llm = LiteLLM(model="anthropic/claude-sonnet-5")
>>> response = llm.generate("Hello!")
"""
@@ -54,7 +54,7 @@ class LiteLLM:
Args:
model: Model identifier in format "provider/model-name"
Examples: "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514",
Examples: "openai/gpt-4o", "anthropic/claude-sonnet-5",
"groq/llama-3.1-8b-instant", "azure/gpt-4", etc.
api_key: API key (optional, can use environment variables)
**kwargs: Additional LiteLLM options (temperature, max_tokens, etc.)
+12
View File
@@ -162,6 +162,13 @@ from .ontology_validator import (
run_shacl_validation,
validate_ontology,
)
from .quality_gate import (
OntologyQualityGate,
OntologyQualityReport,
QualityIssue,
QualitySeverity,
ontology_quality_check,
)
from .owl_generator import OWLGenerator
from .property_generator import PropertyGenerator
from .registry import MethodRegistry, method_registry
@@ -194,6 +201,11 @@ __all__ = [
"SHACLValidationReport",
"SHACLViolation",
"run_shacl_validation",
"OntologyQualityGate",
"OntologyQualityReport",
"QualityIssue",
"QualitySeverity",
"ontology_quality_check",
# OWL/RDF generation
"OWLGenerator",
# Requirements and competency questions
+13
View File
@@ -9,6 +9,7 @@ from .property_generator import PropertyGenerator
from .owl_generator import OWLGenerator
from .ontology_evaluator import OntologyEvaluator
from .ontology_validator import OntologyValidator
from .quality_gate import OntologyQualityGate, OntologyQualityReport
from .llm_generator import LLMOntologyGenerator
from ..semantic_extract.triplet_extractor import Triplet
@@ -25,6 +26,9 @@ class OntologyEngine:
self.owl = OWLGenerator(**config)
self.evaluator = OntologyEvaluator(**config)
self.validator = OntologyValidator(**config)
self.quality_gate = OntologyQualityGate(
validator=self.validator, evaluator=self.evaluator
)
self.llm = LLMOntologyGenerator(**config)
self.store = config.get("store")
@@ -593,6 +597,15 @@ class OntologyEngine:
def validate(self, ontology: Dict[str, Any], **options):
return self.validator.validate(ontology, **options)
def quality_check(
self,
ontology: Dict[str, Any],
graph_data: Optional[Dict[str, Any]] = None,
**options,
) -> OntologyQualityReport:
"""Run deterministic ontology quality checks suitable for CI."""
return self.quality_gate.check(ontology, graph_data=graph_data, **options)
def to_owl(self, ontology: Dict[str, Any], format: str = "turtle", **options):
return self.owl.generate_owl(ontology, format=format, **options)
+855
View File
@@ -0,0 +1,855 @@
"""Deterministic quality checks for ontology and KG pipelines."""
import copy
import math
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, Iterable, List, Mapping, Optional, Set, Tuple
from ..kg.graph_validator import GraphValidator
from .ontology_evaluator import OntologyEvaluator
from .ontology_validator import OntologyValidator
class QualitySeverity(str, Enum):
"""Severity assigned to a quality finding."""
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
@dataclass
class QualityIssue:
"""A single machine-readable ontology quality finding."""
code: str
message: str
severity: QualitySeverity
element_id: Optional[str] = None
element_type: Optional[str] = None
details: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Return a JSON-friendly representation of the issue."""
return {
"code": self.code,
"message": self.message,
"severity": self.severity.value,
"element_id": self.element_id,
"element_type": self.element_type,
"details": self.details,
}
@dataclass
class OntologyQualityReport:
"""Result returned by :class:`OntologyQualityGate`."""
passed: bool
issues: List[QualityIssue] = field(default_factory=list)
stats: Dict[str, int] = field(default_factory=dict)
metrics: Dict[str, float] = field(default_factory=dict)
thresholds: Dict[str, Optional[float]] = field(default_factory=dict)
threshold_failures: List[str] = field(default_factory=list)
@property
def error_count(self) -> int:
"""Number of error and critical findings."""
return sum(
issue.severity in (QualitySeverity.ERROR, QualitySeverity.CRITICAL)
for issue in self.issues
)
@property
def warning_count(self) -> int:
"""Number of warning findings."""
return sum(issue.severity == QualitySeverity.WARNING for issue in self.issues)
@property
def info_count(self) -> int:
"""Number of informational findings."""
return sum(issue.severity == QualitySeverity.INFO for issue in self.issues)
def to_dict(self) -> Dict[str, Any]:
"""Return a JSON-friendly representation of the report."""
return {
"passed": self.passed,
"issues": [issue.to_dict() for issue in self.issues],
"stats": {
**self.stats,
"issues": len(self.issues),
"errors": self.error_count,
"warnings": self.warning_count,
"infos": self.info_count,
},
"metrics": self.metrics,
"thresholds": self.thresholds,
"threshold_failures": self.threshold_failures,
}
class OntologyQualityGate:
"""Run deterministic, CI-friendly ontology quality checks."""
DEFAULT_THRESHOLDS: Dict[str, Optional[float]] = {
"min_coverage": 0.0,
"max_errors": 0.0,
"max_warnings": None,
}
_DATA_PROPERTY_TYPES = {"data", "datatype", "data_property", "literal"}
_OBJECT_PROPERTY_TYPES = {"object", "object_property", "relationship"}
_KNOWN_DATATYPES = {
"string",
"boolean",
"decimal",
"float",
"double",
"integer",
"int",
"long",
"short",
"byte",
"date",
"datetime",
"datetimestamp",
"time",
"duration",
"anyuri",
}
_BUILTIN_CLASSES = {
"owl:thing",
"rdfs:resource",
"http://www.w3.org/2002/07/owl#thing",
"http://www.w3.org/2000/01/rdf-schema#resource",
}
def __init__(
self,
validator: Optional[OntologyValidator] = None,
evaluator: Optional[OntologyEvaluator] = None,
thresholds: Optional[Mapping[str, Optional[float]]] = None,
fail_on_warnings: bool = False,
) -> None:
self.validator = validator or OntologyValidator()
self.evaluator = evaluator or OntologyEvaluator()
self.thresholds = dict(self.DEFAULT_THRESHOLDS)
if thresholds:
self.thresholds.update(thresholds)
self.fail_on_warnings = fail_on_warnings
def check(
self,
ontology: Any,
graph_data: Optional[Dict[str, Any]] = None,
*,
thresholds: Optional[Mapping[str, Optional[float]]] = None,
fail_on_warnings: Optional[bool] = None,
competency_questions: Optional[List[str]] = None,
) -> OntologyQualityReport:
"""Check an ontology and optionally its instance graph.
``graph_data`` is optional because an ontology can be checked before
instances are available. When omitted, embedded ``entities`` and
``relationships`` are checked when present.
"""
active_thresholds = dict(self.thresholds)
if thresholds:
active_thresholds.update(thresholds)
min_coverage, max_errors, max_warnings = self._validate_thresholds(
active_thresholds
)
should_fail_on_warnings = (
self.fail_on_warnings if fail_on_warnings is None else fail_on_warnings
)
issues: List[QualityIssue] = []
if not isinstance(ontology, dict):
self._add_issue(
issues,
"INVALID_ONTOLOGY",
"Ontology must be a dictionary.",
QualitySeverity.CRITICAL,
element_type="ontology",
)
return self._build_report(
issues,
classes=0,
properties=0,
entities=0,
relationships=0,
metrics={
"coverage": 0.0,
"class_coverage": 0.0,
"property_coverage": 0.0,
},
thresholds=active_thresholds,
min_coverage=min_coverage,
max_errors=max_errors,
max_warnings=max_warnings,
fail_on_warnings=should_fail_on_warnings,
)
validation = self.validator.validate(ontology)
for message in getattr(validation, "errors", []) or []:
self._add_issue(
issues,
"VALIDATOR_ERROR",
str(message),
QualitySeverity.ERROR,
element_type="ontology",
)
for message in getattr(validation, "warnings", []) or []:
self._add_issue(
issues,
"VALIDATOR_WARNING",
str(message),
QualitySeverity.WARNING,
element_type="ontology",
)
classes = self._read_collection(ontology, "classes", issues)
properties = self._read_collection(ontology, "properties", issues)
class_aliases, class_ids = self._index_elements(
classes, "class", "MISSING_CLASS_ID", issues
)
referenced_classes: Set[str] = set()
self._mark_hierarchy(classes, class_aliases, referenced_classes)
property_with_endpoints = 0
for index, prop in enumerate(properties):
if not isinstance(prop, dict):
self._add_issue(
issues,
"INVALID_PROPERTY",
f"Property at index {index} must be a dictionary.",
QualitySeverity.ERROR,
element_type="property",
details={"index": index},
)
continue
prop_id = self._identifier(prop)
if prop_id is None:
prop_id = f"property[{index}]"
self._add_issue(
issues,
"MISSING_PROPERTY_ID",
f"Property at index {index} has no name or URI.",
QualitySeverity.ERROR,
element_type="property",
details={"index": index},
)
raw_prop_type = prop.get("type")
prop_type = (
str(raw_prop_type).strip().lower() if raw_prop_type is not None else ""
)
if not prop_type:
self._add_issue(
issues,
"MISSING_PROPERTY_TYPE",
f"Property '{prop_id}' has no type.",
QualitySeverity.ERROR,
element_id=prop_id,
element_type="property",
)
elif (
prop_type not in self._DATA_PROPERTY_TYPES | self._OBJECT_PROPERTY_TYPES
):
self._add_issue(
issues,
"UNKNOWN_PROPERTY_TYPE",
f"Property '{prop_id}' has unknown type '{prop_type}'.",
QualitySeverity.WARNING,
element_id=prop_id,
element_type="property",
)
domains = self._values(prop, "domain")
ranges = self._values(prop, "range")
if domains or ranges:
property_with_endpoints += 1
else:
self._add_issue(
issues,
"ORPHAN_PROPERTY",
f"Property '{prop_id}' has no domain or range.",
QualitySeverity.WARNING,
element_id=prop_id,
element_type="property",
)
if not domains:
self._add_issue(
issues,
"MISSING_DOMAIN",
f"Property '{prop_id}' has no domain.",
QualitySeverity.WARNING,
element_id=prop_id,
element_type="property",
)
for domain in domains:
matched = self._match_class(domain, class_aliases)
if matched:
referenced_classes.add(matched)
elif not self._is_builtin_class(domain):
self._add_issue(
issues,
"UNKNOWN_DOMAIN",
f"Property '{prop_id}' references unknown domain '{domain}'.",
QualitySeverity.ERROR,
element_id=prop_id,
element_type="property",
details={"domain": domain},
)
if not ranges:
self._add_issue(
issues,
"MISSING_RANGE",
f"Property '{prop_id}' has no range.",
QualitySeverity.WARNING,
element_id=prop_id,
element_type="property",
)
for range_value in ranges:
matched = self._match_class(range_value, class_aliases)
if matched:
referenced_classes.add(matched)
self._check_range(
prop_id,
prop_type,
range_value,
matched is not None,
issues,
)
graph = graph_data
if graph is None and ("entities" in ontology or "relationships" in ontology):
graph = ontology
graph_entities, graph_relationships = self._read_graph(graph, issues)
if self._has_valid_graph_shape(graph):
self._check_graph(graph, issues)
self._mark_graph_types(graph_entities, class_aliases, referenced_classes)
for class_id in class_ids:
if class_id not in referenced_classes:
self._add_issue(
issues,
"ORPHAN_CLASS",
f"Class '{class_id}' is not connected to a property, hierarchy, or graph entity type.",
QualitySeverity.WARNING,
element_id=class_id,
element_type="class",
)
class_coverage = (
len(referenced_classes & set(class_ids)) / len(class_ids)
if class_ids
else 0.0
)
property_coverage = (
property_with_endpoints / len(properties) if properties else 1.0
)
metrics: Dict[str, float] = {
"coverage": (
(class_coverage + property_coverage) / 2
if classes or properties
else 0.0
),
"class_coverage": class_coverage,
"property_coverage": property_coverage,
"validator_valid": 1.0 if getattr(validation, "valid", True) else 0.0,
}
if competency_questions is not None:
evaluation_ontology = self._prepare_for_evaluation(
ontology, classes, properties
)
evaluation = self._evaluate_competency_questions(
evaluation_ontology, competency_questions=competency_questions
)
metrics["competency_question_coverage"] = evaluation.coverage_score
metrics["completeness"] = evaluation.completeness_score
return self._build_report(
issues,
classes=len(classes),
properties=len(properties),
entities=len(graph_entities),
relationships=len(graph_relationships),
metrics=metrics,
thresholds=active_thresholds,
min_coverage=min_coverage,
max_errors=max_errors,
max_warnings=max_warnings,
fail_on_warnings=should_fail_on_warnings,
)
def _check_range(
self,
prop_id: str,
prop_type: str,
range_value: Any,
is_class: bool,
issues: List[QualityIssue],
) -> None:
if prop_type in self._DATA_PROPERTY_TYPES:
if not self._is_known_datatype(range_value):
self._add_issue(
issues,
"INVALID_DATATYPE_RANGE",
f"Data property '{prop_id}' has invalid range '{range_value}'.",
QualitySeverity.ERROR,
element_id=prop_id,
element_type="property",
details={"range": range_value},
)
elif prop_type in self._OBJECT_PROPERTY_TYPES:
if not is_class and not self._is_builtin_class(range_value):
self._add_issue(
issues,
"UNKNOWN_RANGE",
f"Object property '{prop_id}' references unknown range '{range_value}'.",
QualitySeverity.ERROR,
element_id=prop_id,
element_type="property",
details={"range": range_value},
)
elif (
not is_class
and not self._is_builtin_class(range_value)
and not self._is_known_datatype(range_value)
):
self._add_issue(
issues,
"UNKNOWN_RANGE",
f"Property '{prop_id}' references unknown range '{range_value}'.",
QualitySeverity.ERROR,
element_id=prop_id,
element_type="property",
details={"range": range_value},
)
def _build_report(
self,
issues: List[QualityIssue],
*,
classes: int,
properties: int,
entities: int,
relationships: int,
metrics: Dict[str, float],
thresholds: Dict[str, Optional[float]],
min_coverage: float,
max_errors: float,
max_warnings: Optional[float],
fail_on_warnings: bool,
) -> OntologyQualityReport:
failures: List[str] = []
error_count = sum(
issue.severity in (QualitySeverity.ERROR, QualitySeverity.CRITICAL)
for issue in issues
)
warning_count = sum(
issue.severity == QualitySeverity.WARNING for issue in issues
)
if error_count > max_errors:
failures.append("max_errors")
if max_warnings is not None and warning_count > max_warnings:
failures.append("max_warnings")
if fail_on_warnings and warning_count:
failures.append("fail_on_warnings")
if metrics.get("coverage", 0.0) < min_coverage:
failures.append("min_coverage")
return OntologyQualityReport(
passed=not failures,
issues=issues,
stats={
"classes": classes,
"properties": properties,
"entities": entities,
"relationships": relationships,
},
metrics=metrics,
thresholds=thresholds,
threshold_failures=failures,
)
@staticmethod
def _validate_thresholds(
thresholds: Mapping[str, Optional[float]],
) -> Tuple[float, float, Optional[float]]:
min_coverage = float(thresholds.get("min_coverage", 0.0) or 0.0)
max_errors = float(thresholds.get("max_errors", 0.0) or 0.0)
warning_value = thresholds.get("max_warnings")
max_warnings = None if warning_value is None else float(warning_value)
if not all(
math.isfinite(value)
for value in (min_coverage, max_errors)
if value is not None
) or (max_warnings is not None and not math.isfinite(max_warnings)):
raise ValueError("quality thresholds must be finite numbers")
if not 0.0 <= min_coverage <= 1.0:
raise ValueError("min_coverage must be between 0.0 and 1.0")
if max_errors < 0 or (max_warnings is not None and max_warnings < 0):
raise ValueError("error and warning thresholds cannot be negative")
return min_coverage, max_errors, max_warnings
@classmethod
def _prepare_for_evaluation(
cls,
ontology: Dict[str, Any],
classes: Iterable[Any],
properties: Iterable[Any],
) -> Dict[str, Any]:
"""Make a shallow, evaluator-safe view without changing caller data."""
prepared = dict(ontology)
prepared["classes"] = [
cls._prepare_element(element)
for element in classes
if isinstance(element, dict)
]
prepared["properties"] = [
cls._prepare_element(element)
for element in properties
if isinstance(element, dict)
]
return prepared
@classmethod
def _prepare_element(cls, element: Dict[str, Any]) -> Dict[str, Any]:
prepared = dict(element)
identifier = cls._identifier(prepared)
if identifier is not None and not str(prepared.get("name", "")).strip():
prepared["name"] = identifier
return prepared
def _evaluate_competency_questions(
self, ontology: Dict[str, Any], competency_questions: List[str]
) -> Any:
"""Evaluate with an isolated question manager for repeatable checks."""
evaluator = copy.copy(self.evaluator)
manager = getattr(self.evaluator, "competency_questions_manager", None)
if manager is not None and hasattr(manager, "questions"):
isolated_manager = copy.copy(manager)
isolated_manager.questions = []
evaluator.competency_questions_manager = isolated_manager
return evaluator.evaluate_ontology(
ontology, competency_questions=competency_questions
)
@classmethod
def _read_collection(
cls, ontology: Dict[str, Any], key: str, issues: List[QualityIssue]
) -> List[Any]:
if key not in ontology:
cls._add_issue(
issues,
f"MISSING_{key.upper()}",
f"Ontology has no {key} defined.",
QualitySeverity.WARNING,
element_type="ontology",
)
return []
value = ontology[key]
if not isinstance(value, list):
cls._add_issue(
issues,
f"INVALID_{key.upper()}",
f"Ontology '{key}' must be a list.",
QualitySeverity.ERROR,
element_type="ontology",
)
return []
return value
@classmethod
def _index_elements(
cls,
elements: Iterable[Any],
element_type: str,
missing_code: str,
issues: List[QualityIssue],
) -> Tuple[Dict[str, str], List[str]]:
aliases: Dict[str, str] = {}
identifiers: List[str] = []
for index, element in enumerate(elements):
identifier = cls._identifier(element)
if identifier is None:
cls._add_issue(
issues,
missing_code,
f"{element_type.title()} at index {index} has no name or URI.",
QualitySeverity.ERROR,
element_type=element_type,
details={"index": index},
)
continue
identifiers.append(identifier)
aliases_for_element: Set[str] = set()
for candidate in cls._identifiers(element):
aliases_for_element.update(cls._term_aliases(candidate))
for alias in sorted(aliases_for_element):
aliases.setdefault(alias, identifier)
return aliases, identifiers
@classmethod
def _mark_hierarchy(
cls,
classes: Iterable[Any],
aliases: Mapping[str, str],
referenced: Set[str],
) -> None:
for class_entry in classes:
if not isinstance(class_entry, dict):
continue
identifier = cls._identifier(class_entry)
for key in (
"subClassOf",
"subclassOf",
"parent",
"superclass",
"superclasses",
):
parents = cls._values(class_entry, key)
if parents and identifier:
referenced.add(identifier)
for parent in parents:
matched = cls._match_class(parent, aliases)
if matched:
referenced.add(matched)
@classmethod
def _mark_graph_types(
cls,
entities: Iterable[Any],
aliases: Mapping[str, str],
referenced: Set[str],
) -> None:
for entity in entities:
if not isinstance(entity, dict):
continue
entity_type = entity.get("type") or entity.get("entity_type")
matched = cls._match_class(entity_type, aliases)
if matched:
referenced.add(matched)
@classmethod
def _check_graph(cls, graph: Dict[str, Any], issues: List[QualityIssue]) -> None:
safe_graph = cls._prepare_graph_for_validation(graph, issues)
result = GraphValidator().validate(safe_graph)
code_map = {
"DANGLING_EDGE": "UNRESOLVED_RELATIONSHIP_ENDPOINT",
"ORPHAN_NODES": "ORPHAN_ENTITY",
}
severity_map = {
"info": QualitySeverity.INFO,
"warning": QualitySeverity.WARNING,
"error": QualitySeverity.ERROR,
"critical": QualitySeverity.CRITICAL,
}
for graph_issue in result.issues:
severity = severity_map.get(
graph_issue.severity.value, QualitySeverity.ERROR
)
details = dict(graph_issue.details or {})
if graph_issue.code == "ORPHAN_NODES" and "ids" in details:
details["ids"] = sorted(details["ids"], key=str)
cls._add_issue(
issues,
code_map.get(graph_issue.code, graph_issue.code),
graph_issue.message,
severity,
element_id=graph_issue.element_id,
element_type=graph_issue.element_type,
details=details,
)
@classmethod
def _prepare_graph_for_validation(
cls, graph: Dict[str, Any], issues: List[QualityIssue]
) -> Dict[str, List[Dict[str, Any]]]:
"""Normalize supported graph aliases and isolate malformed members."""
entities: List[Dict[str, Any]] = []
raw_entities = graph.get("entities", [])
raw_relationships = graph.get("relationships", [])
for index, entity in enumerate(
raw_entities if isinstance(raw_entities, list) else []
):
if not isinstance(entity, dict):
cls._add_issue(
issues,
"INVALID_ENTITY",
f"Entity at index {index} must be a dictionary.",
QualitySeverity.ERROR,
element_type="entity",
details={"index": index},
)
continue
normalized = dict(entity)
if not normalized.get("name") and normalized.get("text") is not None:
normalized["name"] = normalized["text"]
entities.append(normalized)
relationships: List[Dict[str, Any]] = []
for index, relationship in enumerate(
raw_relationships if isinstance(raw_relationships, list) else []
):
if not isinstance(relationship, dict):
cls._add_issue(
issues,
"INVALID_RELATIONSHIP",
f"Relationship at index {index} must be a dictionary.",
QualitySeverity.ERROR,
element_type="relationship",
details={"index": index},
)
continue
relationships.append(dict(relationship))
return {"entities": entities, "relationships": relationships}
@classmethod
def _read_graph(
cls, graph: Optional[Dict[str, Any]], issues: List[QualityIssue]
) -> Tuple[List[Any], List[Any]]:
if graph is None:
return [], []
if not isinstance(graph, dict):
cls._add_issue(
issues,
"INVALID_GRAPH",
"Graph data must be a dictionary.",
QualitySeverity.ERROR,
element_type="graph",
)
return [], []
entities = graph.get("entities", [])
relationships = graph.get("relationships", [])
if not isinstance(entities, list) or not isinstance(relationships, list):
cls._add_issue(
issues,
"INVALID_GRAPH",
"Graph entities and relationships must be lists.",
QualitySeverity.ERROR,
element_type="graph",
)
return [], []
return entities, relationships
@staticmethod
def _has_valid_graph_shape(graph: Optional[Dict[str, Any]]) -> bool:
return bool(
isinstance(graph, dict)
and isinstance(graph.get("entities", []), list)
and isinstance(graph.get("relationships", []), list)
)
@staticmethod
def _identifier(element: Any) -> Optional[str]:
identifiers = OntologyQualityGate._identifiers(element)
return identifiers[0] if identifiers else None
@staticmethod
def _identifiers(element: Any) -> List[str]:
if isinstance(element, dict):
values = []
for key in ("name", "uri", "id", "@id"):
value = element.get(key)
if value is not None and str(value).strip():
values.append(str(value).strip())
return values
if isinstance(element, str) and element.strip():
return [element.strip()]
return []
@staticmethod
def _values(element: Dict[str, Any], key: str) -> List[Any]:
value = element.get(key)
if value is None:
return []
if isinstance(value, (list, tuple, set)):
values = [item for item in value if item is not None and str(item).strip()]
return sorted(values, key=str) if isinstance(value, set) else values
return [value] if str(value).strip() else []
@classmethod
def _term_aliases(cls, value: Any) -> Set[str]:
if isinstance(value, dict):
value = cls._identifier(value)
if value is None:
return set()
text = str(value).strip().strip("<>")
if not text:
return set()
aliases = {text, text.lower()}
for separator in ("#", "/"):
if separator in text:
local = text.rstrip("/").rsplit(separator, 1)[-1]
aliases.update({local, local.lower()})
if ":" in text and not text.startswith(("http://", "https://")):
local = text.rsplit(":", 1)[-1]
aliases.update({local, local.lower()})
return aliases
@classmethod
def _match_class(cls, value: Any, aliases: Mapping[str, str]) -> Optional[str]:
for alias in sorted(cls._term_aliases(value)):
if alias in aliases:
return aliases[alias]
return None
@classmethod
def _is_builtin_class(cls, value: Any) -> bool:
return any(alias in cls._BUILTIN_CLASSES for alias in cls._term_aliases(value))
@classmethod
def _is_known_datatype(cls, value: Any) -> bool:
return any(alias in cls._KNOWN_DATATYPES for alias in cls._term_aliases(value))
@staticmethod
def _add_issue(
issues: List[QualityIssue],
code: str,
message: str,
severity: QualitySeverity,
*,
element_id: Optional[str] = None,
element_type: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
) -> None:
issues.append(
QualityIssue(
code=code,
message=message,
severity=severity,
element_id=element_id,
element_type=element_type,
details=details or {},
)
)
def ontology_quality_check(
ontology: Any,
graph_data: Optional[Dict[str, Any]] = None,
*,
thresholds: Optional[Mapping[str, Optional[float]]] = None,
fail_on_warnings: bool = False,
competency_questions: Optional[List[str]] = None,
validator: Optional[OntologyValidator] = None,
evaluator: Optional[OntologyEvaluator] = None,
) -> OntologyQualityReport:
"""Convenience wrapper around :class:`OntologyQualityGate`."""
gate = OntologyQualityGate(
validator=validator,
evaluator=evaluator,
thresholds=thresholds,
fail_on_warnings=fail_on_warnings,
)
return gate.check(
ontology,
graph_data=graph_data,
competency_questions=competency_questions,
)
@@ -0,0 +1,308 @@
from types import SimpleNamespace
import pytest
from semantica.ontology import (
OntologyEngine,
OntologyQualityGate,
QualitySeverity,
ontology_quality_check,
)
def _ontology():
return {
"classes": [
{"name": "Person", "uri": "https://example.org/Person"},
{"name": "Company", "uri": "https://example.org/Company"},
],
"properties": [
{
"name": "worksFor",
"type": "object",
"domain": ["Person"],
"range": ["Company"],
},
{
"name": "name",
"type": "data",
"domain": ["Person"],
"range": "string",
},
],
}
def test_quality_gate_reports_a_healthy_ontology():
report = ontology_quality_check(_ontology())
assert report.passed
assert report.metrics["coverage"] == 1.0
assert report.error_count == 0
assert report.to_dict()["stats"]["properties"] == 2
def test_quality_gate_finds_schema_and_endpoint_problems():
ontology = {
"classes": [{"name": "Person"}, {"name": "Unused"}],
"properties": [
{
"name": "worksFor",
"type": "object",
"domain": ["Person"],
"range": ["MissingCompany"],
},
{"name": "unattached", "type": "data"},
],
}
graph = {
"entities": [{"id": "p1", "type": "Person"}],
"relationships": [
{"source_id": "p1", "target_id": "missing", "type": "worksFor"}
],
}
report = OntologyQualityGate().check(ontology, graph_data=graph)
codes = {issue.code for issue in report.issues}
assert not report.passed
assert "UNKNOWN_RANGE" in codes
assert "UNRESOLVED_RELATIONSHIP_ENDPOINT" in codes
assert "ORPHAN_CLASS" in codes
assert "MISSING_RANGE" in codes
assert any(issue.severity == QualitySeverity.WARNING for issue in report.issues)
def test_quality_gate_supports_thresholds_and_legacy_endpoint_keys():
ontology = {
"classes": [{"name": "Person"}],
"properties": [
{
"name": "name",
"type": "data",
"domain": "Person",
"range": "xsd:string",
}
],
}
graph = {
"entities": [{"entity_id": "p1", "type": "Person", "name": "Alice"}],
"relationships": [{"source": "p1", "target": "p1", "type": "knows"}],
}
report = ontology_quality_check(
ontology,
graph_data=graph,
thresholds={"min_coverage": 1.0},
)
assert report.passed
assert report.threshold_failures == []
assert report.stats["relationships"] == 1
def test_quality_gate_can_fail_on_warnings():
report = ontology_quality_check(
{"classes": [{"name": "Person"}], "properties": []},
fail_on_warnings=True,
)
assert not report.passed
assert "fail_on_warnings" in report.threshold_failures
def test_engine_exposes_quality_check():
report = OntologyEngine().quality_check(_ontology())
assert report.passed
def test_quality_gate_accepts_canonical_context_graph_entities_without_mutation():
graph = {
"entities": [
{"id": "p1", "text": "Alice", "type": "Person"},
{"id": "c1", "text": "Acme", "type": "Company"},
],
"relationships": [{"source_id": "p1", "target_id": "c1", "type": "worksFor"}],
}
report = OntologyQualityGate().check(_ontology(), graph_data=graph)
assert report.passed
assert "name" not in graph["entities"][0]
def test_quality_gate_returns_structured_findings_for_malformed_graph_members():
report = OntologyQualityGate().check(
_ontology(), graph_data={"entities": [None], "relationships": [None]}
)
codes = {issue.code for issue in report.issues}
assert not report.passed
assert {"INVALID_ENTITY", "INVALID_RELATIONSHIP"} <= codes
def test_quality_gate_returns_structured_finding_for_invalid_graph_containers():
report = OntologyQualityGate().check(
_ontology(), graph_data={"entities": None, "relationships": []}
)
assert not report.passed
assert any(issue.code == "INVALID_GRAPH" for issue in report.issues)
def test_quality_gate_does_not_undercount_identical_dangling_edges():
graph = {
"entities": [{"id": "p1", "name": "Alice", "type": "Person"}],
"relationships": [
{"source": "p1", "target": "missing", "type": "knows"},
{"source": "p1", "target": "missing", "type": "knows"},
],
}
report = OntologyQualityGate().check(
_ontology(), graph_data=graph, thresholds={"max_errors": 1}
)
endpoint_errors = [
issue
for issue in report.issues
if issue.code == "UNRESOLVED_RELATIONSHIP_ENDPOINT"
]
assert len(endpoint_errors) == 2
assert not report.passed
assert "max_errors" in report.threshold_failures
def test_quality_gate_indexes_all_class_identifier_aliases():
ontology = {
"classes": [{"name": "Person", "uri": "https://example.org/PersonType"}],
"properties": [
{
"name": "name",
"type": "data",
"domain": "https://example.org/PersonType",
"range": "string",
}
],
}
report = OntologyQualityGate().check(ontology)
codes = {issue.code for issue in report.issues}
assert "UNKNOWN_DOMAIN" not in codes
def test_quality_gate_recognizes_subclass_of_hierarchies():
ontology = {
"classes": [
{"name": "Person"},
{"name": "Employee", "subclassOf": "Person"},
],
"properties": [
{
"name": "name",
"type": "data",
"domain": "Employee",
"range": "string",
}
],
}
report = OntologyQualityGate().check(ontology)
assert not any(issue.code == "ORPHAN_CLASS" for issue in report.issues)
def test_quality_gate_keeps_competency_evaluation_safe_for_malformed_members():
ontology = {
"classes": [None, {"uri": "https://example.org/Person"}],
"properties": [
None,
{
"uri": "https://example.org/name",
"type": "data",
"domain": "Person",
"range": "string",
},
],
}
report = OntologyQualityGate().check(
ontology, competency_questions=["What is a person's name?"]
)
assert isinstance(report.metrics["competency_question_coverage"], float)
def test_quality_gate_reports_validator_warnings_and_can_fail_on_them():
class WarningValidator:
def validate(self, ontology):
return SimpleNamespace(valid=True, errors=[], warnings=["review me"])
report = OntologyQualityGate(
validator=WarningValidator(), fail_on_warnings=True
).check(_ontology())
assert report.warning_count == 1
assert "VALIDATOR_WARNING" in {issue.code for issue in report.issues}
assert "fail_on_warnings" in report.threshold_failures
def test_quality_gate_isolates_competency_questions_between_checks():
engine = OntologyEngine()
first = engine.quality_check(_ontology(), competency_questions=["Who is a Person?"])
second = engine.quality_check(
_ontology(), competency_questions=["What is a location?"]
)
assert first.metrics["competency_question_coverage"] == 1.0
assert second.metrics["competency_question_coverage"] == 0.0
def test_quality_gate_treats_null_property_type_as_missing():
ontology = {
"classes": [{"name": "Person"}],
"properties": [
{
"name": "value",
"type": None,
"domain": "Person",
"range": "string",
}
],
}
report = OntologyQualityGate().check(ontology)
assert "MISSING_PROPERTY_TYPE" in {issue.code for issue in report.issues}
assert report.error_count >= 1
def test_quality_gate_normalizes_orphan_ids_for_deterministic_reports():
graph = {
"entities": [
{"id": "p2", "name": "Bob", "type": "Person"},
{"id": "p1", "name": "Alice", "type": "Person"},
],
"relationships": [],
}
report = OntologyQualityGate().check(_ontology(), graph_data=graph)
orphan = next(issue for issue in report.issues if issue.code == "ORPHAN_ENTITY")
assert orphan.details["ids"] == ["p1", "p2"]
@pytest.mark.parametrize(
"thresholds",
[
{"min_coverage": float("nan")},
{"max_errors": float("inf")},
{"max_warnings": float("-inf")},
],
)
def test_quality_gate_rejects_non_finite_thresholds(thresholds):
with pytest.raises(ValueError, match="finite"):
OntologyQualityGate().check(_ontology(), thresholds=thresholds)