Compare commits

..
Author SHA1 Message Date
KaifAhmad1 ef6276eee7 docs: tighten prose in concepts.md, guides/graphrag.md, reference/context.md
Flagship pass establishing the crisp-prose style for the rest of
docs/: remove em dashes from explanatory prose (leave them in
simulated document/alert string literals, which are data, not our
voice), replace colon-as-dramatic-pause constructions, and fix two
broken relative links in reference/context.md ([Reasoning](reasoning)
and [Provenance](provenance) were missing their leading slash and
would 404 on the live site, the same class of bug fixed sitewide in
PR #1407). concepts.md's intro also picks up the new context/semantic
layer tagline. No code examples, tables, or technical content
changed.
2026-09-03 17:13:08 +05:30
26 changed files with 566 additions and 1313 deletions
+1 -7
View File
@@ -28,13 +28,7 @@
[![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=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)
[![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)
```bash
pip install semantica
+19 -32
View File
@@ -132,17 +132,16 @@ stats = context.store(
print("Graph built: {} nodes, {} edges".format(
stats["graph_nodes"], stats["graph_edges"]
))
# Graph built: 18 nodes, 14 edges
# Nodes: APT29, HAMMERTOSS, NATO, LifeCare, AS59796, CISA Sector 6, ...
# Edges: deployed, observed_on, classified_as, targets, operates_in, ...
```
`store()` returns a dict with `stored_count`, `memory_ids`, `graph_nodes`, and
`graph_edges`. The extracted nodes (APT29, HAMMERTOSS, LifeCare, AS59796, …) and
edges (`deployed`, `observed_on`, `classified_as`, …) now span all four documents.
The graph now contains a connected subgraph linking APT29 to healthcare infrastructure across four document boundaries, something that would be invisible to a pure vector search.
## Retrieving the relevant subgraph
With the graph populated, a plain `retrieve()` call already does more than vector search. When `use_graph=True`, the retriever seeds the graph traversal from the top-k vector matches and expands outward by following edges. Expansion depth is set once, by `max_expansion_hops` on the `AgentContext` constructor:
With the graph populated, a plain `retrieve()` call already does more than vector search. When `use_graph=True`, the retriever seeds the graph traversal from the top-k vector matches and expands outward by following edges, collecting connected facts within `max_hops`:
```python
results = context.retrieve(
@@ -150,6 +149,7 @@ results = context.retrieve(
use_graph=True,
max_results=10,
expand_graph=True,
max_hops=3,
)
for r in results:
@@ -175,19 +175,11 @@ apt29_intel = context.retrieve(
use_graph=True,
anchor_node="APT29",
proximity_weight=0.7, # strongly favour nodes close to APT29
max_hops=3, # with an anchor, this bounds the proximity radius
max_hops=3,
max_results=8,
)
```
<Note>
`max_hops` on `retrieve()` only takes effect when `anchor_node` is set: it
bounds the proximity radius used for scoring and drops results farther than
`max_hops` from the anchor. Without an `anchor_node` it is ignored. It does
**not** change how far graph expansion reaches: that is fixed by
`max_expansion_hops` on the constructor.
</Note>
## Getting a grounded LLM answer with a reasoning path
`retrieve()` gives you the grounded context. `query_with_reasoning()` goes one step further: it passes that subgraph context to an LLM and returns the answer together with the multi-hop path the retrieval system traced through the graph. That path is your audit trail.
@@ -195,7 +187,7 @@ apt29_intel = context.retrieve(
```python
from semantica.llms import LiteLLM
llm = LiteLLM(model="anthropic/claude-sonnet-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
result = context.query_with_reasoning(
"What are APT29's known TTPs against healthcare infrastructure, "
@@ -281,7 +273,7 @@ context.store(
link_entities=True,
)
llm = LiteLLM(model="anthropic/claude-sonnet-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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 +343,7 @@ Parent: wmiprvse.exe
Sigma match: T1053.005 Scheduled Task/Job
"""
llm = LiteLLM(model="anthropic/claude-sonnet-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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 +417,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-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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),
@@ -474,17 +466,12 @@ compliance_context = AgentContext(
retention_days=2555, # 7-year regulatory retention
)
# In production the text comes from a parsed file, e.g. FileIngestor().ingest_file(path).text;
# inline strings here for brevity
basel_cre20_text = (
"CRE20.32: For income-producing real estate where repayment depends on "
"property cash flows, RWA = exposure × risk weight, where risk weight "
"is determined by LTV bucket per Table CRE20.3..."
)
bcbs239_text = (
"Principle 3: Risk data should be accurate and have a single authoritative source. "
"Where data is aggregated across systems, reconciliation must be documented..."
)
# In production these come from ingest_file(); shown as strings here for brevity
basel_cre20_text = "CRE20.32: For income-producing real estate where repayment depends on "
"property cash flows, RWA = exposure × risk weight, where risk weight "
"is determined by LTV bucket per Table CRE20.3..."
bcbs239_text = "Principle 3: Risk data should be accurate and have a single authoritative source. "
"Where data is aggregated across systems, reconciliation must be documented..."
compliance_context.store(
[
@@ -495,7 +482,7 @@ compliance_context.store(
extract_relationships=True,
)
llm = LiteLLM(model="anthropic/claude-sonnet-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
answer = compliance_context.query_with_reasoning(
"Under Basel III CRE20, what are the RWA calculation requirements for "
"commercial real estate exposures with LTV > 80%? "
@@ -548,7 +535,7 @@ results = context.retrieve(
)
```
Each additional expansion hop exponentially increases the subgraph size. Practical defaults by domain:
Each additional hop in `max_hops` exponentially increases the subgraph size. Practical defaults by domain:
```text
General Q&A max_expansion_hops=2 (95% of useful facts within 2 hops)
@@ -557,7 +544,7 @@ Drug interactions max_expansion_hops=3 (drug → enzyme → metabolite
Regulatory cross-ref max_expansion_hops=2 (rule → article → article)
```
Expansion depth is a constructor setting only (`max_expansion_hops`); there is no per-call override on `retrieve()`. `query_with_reasoning()` does take a per-call `max_hops` argument.
Set globally in the constructor; override per call with the `max_hops` argument to `retrieve()`.
## How GraphRAG works internally
+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-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.
`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.
```python
from semantica.llms import LiteLLM
# Anthropic Claude — highest accuracy for complex reasoning
llm = LiteLLM(model="anthropic/claude-sonnet-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
# 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-sonnet-4-5-20250929-v1:0")
llm = LiteLLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2: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-5",
"prod": "anthropic/claude-sonnet-4-20250514",
"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-5")
deep_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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-5")
deep_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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-5")
report_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
# 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-5")
claude = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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-5")
deep_llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
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-5")
llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
# --- 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-5")
llm_gen = LLMOntologyGenerator(provider="anthropic", model="claude-sonnet-4-20250514")
ontology = llm_gen.generate_ontology_from_text(
"\n\n".join(r.text[:8000] for r in regs) # token-safe excerpt per document
)
+2 -4
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}") # the final step's return value, e.g. {"node_count": ..., "edge_count": ...}
print(f"Output: {result.output}") # {"node_count": 312, "edge_count": 847}
print(f"Duration: {result.metrics['execution_time']:.2f}s")
print(f"Steps completed: {result.metrics['steps_executed']}")
```
@@ -197,9 +197,7 @@ engine = ExecutionEngine(
max_workers = 4,
retry_on_failure = True,
)
# 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
# The engine uses handler.get_retry_policy(step.step_type) when a step fails
```
`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:
+16 -21
View File
@@ -100,15 +100,14 @@ ner = NamedEntityRecognizer(
methods=["llm", "ml", "pattern"],
confidence_threshold=0.75,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
entities = ner.extract_entities(report)
for e in entities:
print("[{:>5.2f}] {:15s} {}".format(e.confidence, e.label, e.text))
# Illustrative output — exact labels and scores depend on the method and model.
# Abbreviated:
# Expected output (abbreviated):
# [ 0.94] THREAT_ACTOR GAMMA-7
# [ 0.91] THREAT_ACTOR DELTA-3
# [ 0.97] MALWARE HAMMERTOSS
@@ -263,18 +262,16 @@ from semantica.semantic_extract import TripletExtractor
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
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 what it produces
# uses them to constrain and validate 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)))
@@ -323,7 +320,7 @@ def ingest_intel_report(
methods=[method, "pattern"],
confidence_threshold=0.70,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
entities = ner.extract_entities(text)
classified = ner.classify_entities(entities)
@@ -338,7 +335,7 @@ def ingest_intel_report(
relation_types=["deployed", "targets", "exploits", "operates_from", "provided_to"],
confidence_threshold=0.65,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
relations = rel.extract_relations(text, entities)
@@ -350,10 +347,9 @@ def ingest_intel_report(
tri = TripletExtractor(
method=method,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
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)
@@ -381,7 +377,6 @@ 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),
@@ -407,7 +402,7 @@ for text, doc_id in reports:
summary["relations"],
summary["events"],
summary["triplets_valid"],
summary["triplets_total"],
len(summary["rdf_turtle"]),
))
```
@@ -426,7 +421,7 @@ ner = NamedEntityRecognizer(
methods=["llm", "pattern"],
confidence_threshold=0.75,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
entities = ner.extract_entities(fintel_text)
grouped = ner.classify_entities(entities)
@@ -443,14 +438,14 @@ rel = RelationExtractor(
relation_types=["operates_from", "deployed", "targets", "exploits"],
confidence_threshold=0.70,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
relations = rel.extract_relations(fintel_text, entities)
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
include_temporal=True,
include_provenance=True,
)
@@ -549,14 +544,14 @@ rel = RelationExtractor(
relation_types=["treats", "causes_adverse_event", "has_efficacy", "evaluated_in"],
confidence_threshold=0.65,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
relations = rel.extract_relations(paper, entities)
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
triplet_types=["treats", "has_efficacy", "causes_adverse_event"],
include_temporal=True,
include_provenance=True,
@@ -600,7 +595,7 @@ ner = NamedEntityRecognizer(
methods=["llm", "ml", "pattern"],
confidence_threshold=0.70,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
entities = ner.extract_entities(credit_memo)
grouped = ner.classify_entities(entities)
@@ -617,14 +612,14 @@ rel = RelationExtractor(
relation_types=["guaranteed_by", "secured_by", "classified_as", "exposed_to"],
confidence_threshold=0.65,
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
)
relations = rel.extract_relations(credit_memo, entities)
tri = TripletExtractor(
method="llm",
provider="anthropic",
llm_model="claude-sonnet-5",
llm_model="claude-sonnet-4-6",
include_temporal=True,
include_provenance=True,
)
+293 -24
View File
@@ -1,5 +1,5 @@
---
title: "Welcome to Semantica"
title: "Semantica"
description: "The Context and Semantic Layer for AI in High-Stakes Domains: Context Graphs · Decision Intelligence · Full Provenance"
---
@@ -7,25 +7,93 @@ description: "The Context and Semantic Layer for AI in High-Stakes Domains: Cont
pip install semantica
```
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.
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.
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.
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.
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 you get
## What Most AI Stacks Are Missing
- **[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*
**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
<Tip>
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.
Works alongside any LLM provider and any agent framework. Add it to an existing stack without changing your architecture.
</Tip>
## Try it
<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
```
<CodeGroup>
@@ -107,28 +175,229 @@ decision_id = context.record_decision(
</CodeGroup>
## Start here
- [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
<Steps>
<Step title="Install">
<Step title="Install Semantica">
```bash
pip install semantica
```
Optional extras: `[all]`, `[neo4j]`, `[pinecone]`. See [Installation](/installation).
See [Installation](/installation) for optional extras (`[all]`, `[neo4j]`, `[pinecone]`) and environment setup.
</Step>
<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 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>
<Step title="Learn the model">
[Core Concepts](/concepts) covers knowledge graphs vs. vector stores, GraphRAG, and how provenance and decisions fit together.
<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>
<Step title="Go deep">
Every module has a [reference page](/reference/context) with full API docs and runnable examples.
<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>
</Steps>
More: the [Cookbook](/cookbook) for real-world notebooks, [Discord](https://discord.gg/sV34vps5hH) for help.
- [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
<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
+4 -4
View File
@@ -12,13 +12,13 @@ icon: "link"
pip install "semantica[langchain]"
```
Requires `langchain-core >= 0.3`. If langchain-core is not installed, the integration still imports. Every class carries the full Semantica API and degrades gracefully (`build()` returns `None`; branch on `LANGCHAIN_AVAILABLE`).
Requires `langchain-core >= 0.3`. If langchain-core is not installed, the integration still imports — every class carries the full Semantica API and degrades gracefully (`build()` returns `None`; branch on `LANGCHAIN_AVAILABLE`).
## Components at a Glance
- **SemanticaRetriever** (`BaseRetriever`): hybrid-search seeds retrieval, then graph edges are walked `hops` steps (default 2) for GraphRAG-style results.
- **SemanticaVectorStore** (`VectorStore`): `add_texts` / `similarity_search` / `similarity_search_with_score` / `from_texts` over `HybridSearch`.
- **SemanticaKGTool** / **SemanticaDecisionTool** (`BaseTool` subclasses): `semantica_query_graph` and `semantica_query_decisions` for LangGraph / tool-calling agents.
- **SemanticaRetriever** `BaseRetriever`: hybrid-search seeds retrieval, then graph edges are walked `hops` steps (default 2) for GraphRAG-style results.
- **SemanticaVectorStore** `VectorStore`: `add_texts` / `similarity_search` / `similarity_search_with_score` / `from_texts` over `HybridSearch`.
- **SemanticaKGTool** / **SemanticaDecisionTool** `BaseTool` subclasses: `semantica_query_graph` and `semantica_query_decisions` for LangGraph / tool-calling agents.
## Component Details
+121 -162
View File
@@ -28,9 +28,7 @@ Semantica is organized into **27 modules** across six logical layers. Each modul
### Ingest
Loads data from files, web, databases, and streams. Each ingestor returns its own
result type (`FileIngestor``FileObject`, `WebIngestor``WebContent`, …);
document-oriented ones expose a `.text` payload and `.metadata`.
Loads data from files, web, databases, and streams into a unified `SourceDocument` format.
```python
from semantica.ingest import FileIngestor, WebIngestor, ParquetIngestor, XMLIngestor, DatabricksIngestor
@@ -39,7 +37,7 @@ from semantica.ingest import FileIngestor, WebIngestor, ParquetIngestor, XMLInge
ingestor = FileIngestor()
documents = ingestor.ingest_directory("data/")
# Web page: returns a WebContent with .text, .title, .links, .metadata
# Web crawl
web_ingestor = WebIngestor()
page = web_ingestor.ingest_url("https://example.com")
@@ -69,13 +67,13 @@ Extracts structured text and layout metadata from raw documents.
```python
from semantica.parse import DocumentParser, DoclingParser
# Standard parser: all common formats. parse() takes a path, returns a dict
# Standard parser: all common formats
parser = DocumentParser()
parsed = parser.parse("document.pdf") # {"full_text": ..., "metadata": ..., ...}
parsed = parser.parse_document("document.pdf")
# Advanced parser (pip install semantica[parse-docling]): tables, OCR, layout
parser = DoclingParser(export_format="markdown", enable_ocr=True)
parsed = parser.parse("data/annual_report.pdf") # dict with full_text, tables, pages
# Advanced parser: multi-column PDFs, merged-cell tables, OCR
parser = DoclingParser(extract_tables=True, extract_images=True, output_format="markdown")
parsed = parser.parse("data/annual_report.pdf")
```
**Available parsers:** `DocumentParser`, `DoclingParser`, `CodeParser`, `CSVParser`, `DocxParser`, `EmailParser`, `ExcelParser`, `HTMLParser`, `ImageParser`, `JSONParser`, `MCPParser`, `MediaParser`, `PDFParser`, `PPTXParser`, `StructuredDataParser`, `WebParser`, `XMLParser`
@@ -87,12 +85,11 @@ Chunks text for embedding and RAG pipelines with awareness of semantic boundarie
```python
from semantica.split import TextSplitter
# chunk_size / chunk_overlap are constructor arguments
splitter = TextSplitter(method="semantic_transformer", chunk_size=1000, chunk_overlap=200)
chunks = splitter.split(text)
splitter = TextSplitter(method="semantic_transformer")
chunks = splitter.split(text, chunk_size=1000, chunk_overlap=200)
```
**Chunking methods:** `recursive`, `token`, `sentence`, `paragraph`, `semantic_transformer`, `entity_aware`, `relation_aware`, `graph_based`, `ontology_aware`, `hierarchical`, `community_detection`, `centrality_based`, `llm`
**Chunking strategies:** `recursive`, `semantic_transformer`, `entity_aware`, `relation_aware`, `sliding_window`, `structural`
### Normalize
@@ -118,18 +115,17 @@ Named entity recognition, relation extraction, and triplet generation.
```python
from semantica.semantic_extract import NERExtractor, RelationExtractor, TripletExtractor
# LLM method: provider + llm_model select the backend; the API key comes from the env
ner = NERExtractor(method="llm", provider="groq", llm_model="llama-3.3-70b-versatile")
entities = ner.extract("Apple Inc. was founded by Steve Jobs.") # list[Entity]
ner = NERExtractor(method="llm", llm_provider=llm)
entities = ner.extract("Apple Inc. was founded by Steve Jobs.")
rel = RelationExtractor(method="llm", provider="groq", llm_model="llama-3.3-70b-versatile")
relationships = rel.extract(text, entities=entities) # list[Relation]
rel = RelationExtractor(method="llm", llm_provider=llm)
relationships = rel.extract(text, entities=entities)
trip = TripletExtractor(method="pattern")
triplets = trip.extract(text) # list[Triplet]
trip = TripletExtractor(method="llm", llm_provider=llm)
triplets = trip.extract(text)
```
**Extraction methods:** `"pattern"` (no API key), `"ml"` (local spaCy model), `"llm"` (any of the 9 supported providers)
**Extraction methods:** `"pattern"` (no API key), `"ml"` (local model), `"llm"` (any of the 8 supported providers)
**Additional extractors:** `CoreferenceResolver`, `EventDetector`, `SemanticAnalyzer`, `SemanticNetworkExtractor`
@@ -141,17 +137,17 @@ Graph construction, graph algorithms, temporal model, and distance intelligence.
from semantica.kg import GraphBuilder, GraphAnalyzer, TemporalGraphQuery, SimilarityCalculator
from datetime import datetime
# Build: build() takes a {"entities": ..., "relationships": ...} dict
# Build
builder = GraphBuilder(merge_entities=True)
kg = builder.build({"entities": entities, "relationships": relationships})
kg = builder.build(entities=entities, relationships=relationships)
# Temporal graphs (v0.4.0)
query_engine = TemporalGraphQuery(enable_temporal_reasoning=True)
snapshot = query_engine.query_at_time(kg, query="", at_time=datetime(2021, 6, 15))
# Semantic similarity (v0.5.0): operates on embedding vectors
calc = SimilarityCalculator(method="cosine")
score = calc.cosine_similarity(vec_a, vec_b)
# Semantic similarity (v0.5.0)
calc = SimilarityCalculator()
scores = calc.calculate_similarity(entity_a, entity_b)
```
**Graph algorithms available:** centrality calculation, community detection, connectivity analysis, entity resolution, link prediction, path finding, similarity calculation
@@ -179,23 +175,19 @@ Derives new facts from existing knowledge using multiple inference strategies.
```python
from semantica.reasoning import Reasoner, DatalogReasoner
# Forward chaining: facts and rules as predicate(args) / IF-THEN strings
# Rule-based reasoning
engine = Reasoner()
engine.add_fact("Manager(Alice)")
engine.add_rule("IF Manager(?x) THEN HasAuthority(?x)")
results = engine.forward_chain() # list[InferenceResult] with .conclusion, .rule_used
engine.apply_transitivity("located_in")
engine.apply_symmetry("knows")
result = engine.infer()
# Datalog: recursive Horn clause rules (v0.4.0)
datalog = DatalogReasoner()
datalog.add_fact("parent(tom, bob)")
datalog.add_fact("parent(bob, ann)")
datalog.add_rule("ancestor(X, Y) :- parent(X, Y).")
datalog = DatalogEngine()
datalog.add_rule("ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).")
datalog.derive_all()
results = datalog.query("ancestor(tom, ?Z)") # [{"Z": "bob"}, {"Z": "ann"}], order not guaranteed
results = datalog.query("ancestor(alice, ?)")
```
**Engines:** `Reasoner` (forward/backward chaining), `ReteEngine`, `SPARQLReasoner`, `DatalogReasoner`, `TemporalReasoningEngine`, `GraphReasoner` (LLM)
**Engines:** forward chaining, Rete network, deductive, abductive, SPARQL, Datalog: all produce explainable inference paths
## Storage
@@ -207,9 +199,9 @@ Generates and manages vector embeddings for semantic similarity.
```python
from semantica.embeddings import EmbeddingGenerator
generator = EmbeddingGenerator()
embeddings = generator.generate_embeddings(["text1", "text2"]) # np.ndarray
similarity = generator.compare_embeddings(embeddings[0], embeddings[1])
generator = EmbeddingGenerator(model="sentence-transformers")
embeddings = generator.generate(["text1", "text2"])
similarity = generator.similarity(embeddings[0], embeddings[1])
```
**Supported models:** Sentence-Transformers, FastEmbed, OpenAI, BGE
@@ -223,18 +215,12 @@ Multi-backend vector database with hybrid search support.
```python
from semantica.vector_store import VectorStore
store = VectorStore(backend="faiss", dimension=768)
# Raw vectors
ids = store.store_vectors(embeddings) # returns generated ids
hits = store.search_vectors(query_vector, k=10)
# Or store text and let the store embed it
store.add_documents(["Apple was founded in 1976.", "Google was founded in 1998."])
results = store.search("tech company founding dates", limit=10)
store = VectorStore(backend="faiss", dimension=768)
store.add_vectors(embeddings, ids)
results = store.search(query_vector, top_k=10)
```
**Backends:** FAISS, Pinecone, Weaviate, Qdrant, Milvus, PgVector, SQLite, in-memory
**Backends:** FAISS, Pinecone, Weaviate, Qdrant, Milvus, PgVector, in-memory
**Search modes:** semantic top-k, hybrid (vector + keyword), metadata-filtered
@@ -246,8 +232,8 @@ Connects to graph databases for persistent, query-able storage.
from semantica.graph_store import GraphStore
store = GraphStore(backend="neo4j")
store.add_nodes([{"id": "acme", "type": "Organization", "properties": {"name": "Acme"}}])
store.add_edges([{"source": "alice", "target": "acme", "type": "works_for"}])
store.add_nodes(entities)
store.add_edges(relationships)
results = store.query("MATCH (n)-[r]->(m) RETURN n, r, m")
```
@@ -260,9 +246,9 @@ RDF triple-based storage with SPARQL query support.
```python
from semantica.triplet_store import TripletStore
store = TripletStore(backend="oxigraph")
store.add_triplets(triplets) # list of Triplet objects (or add_triplet for one)
results = store.execute_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
store = TripletStore(backend="blazegraph")
store.add_triplets(subject, predicate, obj)
results = store.sparql("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
```
**Backends:** Oxigraph (embedded), Blazegraph, Apache Jena, RDF4J
@@ -275,18 +261,15 @@ results = store.execute_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
Detects, scores, and merges duplicate entities across sources.
```python
from semantica.deduplication import DuplicateDetector, EntityMerger
from semantica.deduplication import EntityResolver
detector = DuplicateDetector(similarity_threshold=0.85)
candidates = detector.detect_duplicates(entities)
merger = EntityMerger()
operations = merger.merge_duplicates(entities, strategy="keep_most_complete")
resolver = EntityResolver()
merged = resolver.resolve(entities, strategy="semantic_v2")
```
**v2 candidate-generation modes** (`blocking_v2`, `hybrid_v2`, `semantic_v2`) are up to 7x faster than v1.
**v2 strategies** (`blocking_v2`, `hybrid_v2`, `semantic_v2`) are up to 7x faster than v1.
**Components:** `DuplicateDetector`, `EntityMerger`, `ClusterBuilder`, `MergeStrategyManager`
**Components:** `EntityResolver`, `DuplicateDetector`, `EntityMerger`, `SimilarityCalculator`, `ClusterBuilder`
**`DuplicateDetector` options:** `max_results`, `top_k_per_entity`, `min_similarity`, `sort_by`
@@ -295,13 +278,14 @@ operations = merger.merge_duplicates(entities, strategy="keep_most_complete")
Detects and resolves fact conflicts across overlapping knowledge sources.
```python
from semantica.conflicts import ConflictDetector, ConflictResolver
from semantica.conflicts import ConflictDetector
conflicts = ConflictDetector().detect_conflicts(entities) # list of entity dicts
resolved = ConflictResolver().resolve_conflicts(conflicts, strategy="most_recent")
detector = ConflictDetector()
conflicts = detector.detect_conflicts(kg)
resolved = detector.resolve(conflicts, strategy="most_recent")
```
**Detection types:** value conflicts, type conflicts, relationship conflicts, temporal conflicts, logical conflicts
**Detection types:** value conflicts, type conflicts, temporal conflicts, logical conflicts
**Resolution strategies:** prefer most recent, prefer most reliable source, majority vote, flag for manual review
@@ -314,7 +298,6 @@ Agent context graphs, decision tracking, causal chains, and precedent search.
```python
from semantica.context import AgentContext, ContextGraph
from semantica.vector_store import VectorStore
context = AgentContext(
vector_store=VectorStore(backend="faiss", dimension=768),
@@ -345,7 +328,7 @@ W3C PROV-O compliant lineage tracking across all modules.
from semantica.provenance import ProvenanceManager
manager = ProvenanceManager()
manager.track_entity("entity_1", source="document.pdf", metadata={"type": "person"})
manager.track_entity("entity_1", "document.pdf", "person")
lineage = manager.get_lineage("entity_1")
```
@@ -381,8 +364,8 @@ RDFExporter().export(graph, file_path="graph.ttl", format="turtle")
# Analytics
ParquetExporter().export(graph, file_path="output/graph.parquet")
# ArangoDB: writes AQL INSERT statements to the given path
ArangoAQLExporter().export(graph, file_path="graph.aql")
# ArangoDB
aql = ArangoAQLExporter().export(graph)
```
**Export formats:** RDF (Turtle, JSON-LD, N-Triples, XML), Parquet, ArangoDB AQL, CSV, OWL, Arrow, LPG, YAML, distance matrices
@@ -407,24 +390,16 @@ viz.visualize_network(graph, output="html", file_path="graph.html")
Pipeline DSL with parallel workers, retry policies, and failure handling.
```python
from semantica.pipeline import PipelineBuilder, ExecutionEngine
from semantica.ingest import FileIngestor
from semantica.semantic_extract import NERExtractor
from semantica.pipeline import Pipeline
builder = PipelineBuilder()
# Each step type dispatches to a handler you register (or supply explicitly)
builder.register_step_handler("ingest", lambda data, **c: FileIngestor().ingest(c["source"]))
builder.register_step_handler("extract", lambda docs, **c: NERExtractor(method="pattern").extract(docs[0].text))
builder.add_step("ingest", step_type="ingest", source="data/")
builder.add_step("extract", step_type="extract")
pipeline = builder.connect_steps("ingest", "extract").build(name="docs_to_entities")
result = ExecutionEngine().execute_pipeline(pipeline)
pipeline = Pipeline()
pipeline.add_step("ingest", FileIngestor())
pipeline.add_step("extract", NERExtractor())
pipeline.add_step("build", GraphBuilder())
result = pipeline.run("data/")
```
**Components:** `PipelineBuilder`, `Pipeline`, `ExecutionEngine`, `FailureHandler`, `PipelineValidator`, `ParallelismManager`, `ResourceScheduler`
**Components:** `Pipeline`, `PipelineBuilder`, `ExecutionEngine`, `FailureHandler`, `PipelineValidator`, `ParallelismManager`, `ResourceScheduler`
### Explorer
@@ -453,7 +428,7 @@ llm = OpenAI(model="gpt-4o", api_key=os.getenv("OPENAI_API_KEY"))
llm = LiteLLM(model="anthropic/claude-opus-4-7", api_key=os.getenv("ANTHROPIC_API_KEY"))
```
**Supported providers:** OpenAI, Anthropic, Google Gemini, Groq, Ollama, DeepSeek, Novita AI, HuggingFace, plus LiteLLM (100+ models via one interface)
**Supported providers:** OpenAI, Anthropic, Google Gemini, Groq, Ollama, DeepSeek, Novita AI, LiteLLM (20+ models via one interface)
### MCP Server
@@ -470,43 +445,44 @@ python -m semantica.mcp_server
Bootstrap knowledge graphs from verified structured sources: fixed-point reference data, controlled vocabularies, and domain anchors.
```python
from semantica.seed import SeedDataManager
from semantica.seed import SeedManager
seed = SeedDataManager()
seed = SeedManager()
seed.populate(kg, dataset="companies", count=100)
# Load trusted reference data from CSV / JSON / a database / an API
seed_data = seed.load_from_csv("seed_data/industries.csv", entity_type="Industry")
# Merge seed data with extraction output (seed values win on conflict by default)
combined = seed.integrate_with_extracted(
{"entities": seed_data, "relationships": []},
{"entities": extracted_entities, "relationships": extracted_relationships},
merge_strategy="seed_first",
)
# Load domain seeds from file or built-in datasets
seed.load_from_file("seed_data/industries.json")
seed.inject(kg) # merges seed nodes without duplicating existing entities
```
**Use cases:** anchoring extraction with known entities, pre-populating ontology classes, deterministic test graph generation.
### Evals
Scores decision-intelligence outputs (decision records, audit trails, reasoning
text) with a registry of deterministic and model-backed evaluators plus a small
run harness.
Evaluation framework for measuring KG quality, extraction accuracy, and pipeline performance.
```python
from semantica.evals import evaluate, list_evaluators
from semantica.evals import KGEvaluator, ExtractionEvaluator, PipelineEvaluator, RegressionTracker
list_evaluators()
# ['decision_scores', 'exact_match', 'keyword_check', 'length_range',
# 'levenshtein', 'llm_as_judge', 'numeric_range', 'regex_match', 'rouge',
# 'temporal_range']
# KG quality
report = KGEvaluator().evaluate(kg, ontology=ontology)
print(f"Completeness: {report.completeness:.2%} Consistency: {report.consistency:.2%}")
cases = [("apple", "aple"), ("night", "nacht")]
summary = evaluate(cases, evaluators=["levenshtein"])
print(summary.total, summary.passed, summary.pass_rate)
# Extraction accuracy
report = ExtractionEvaluator().evaluate_ner(predictions=extracted, gold_standard=annotated)
print(f"Precision: {report.precision:.3f} Recall: {report.recall:.3f} F1: {report.f1:.3f}")
# Pipeline throughput and latency
metrics = PipelineEvaluator().benchmark(pipeline, data="data/", bench_runs=5)
print(f"Throughput: {metrics.docs_per_second:.1f} docs/sec")
# Regression tracking across runs
tracker = RegressionTracker(db_path="eval_history.db")
run_id = tracker.record_run(pipeline_version="v1.2.0", metrics=metrics)
diff = tracker.compare(run_id, baseline_run_id="run_abc123")
```
**Public API:** `evaluate(cases, evaluators, config=None)`, `list_evaluators()`, `get_evaluator(name)`, and the `EvalMetric` / `CaseResult` / `EvalSummary` result types. See the [Evals reference](/reference/evals).
**Components:** `KGEvaluator`, `ExtractionEvaluator`, `PipelineEvaluator`, `RegressionTracker`
### Core
@@ -515,20 +491,20 @@ Base classes, shared data models, and the plugin registry used across all module
```python
from semantica.core import Semantica, PluginRegistry, ConfigManager
# ConfigManager loads a Config; Config.get() does dotted lookups
config = ConfigManager().load_from_file("config.yaml")
batch = config.get("processing.batch_size", default=32)
# Top-level orchestrator: pass the Config object (or a dict), not a path
sem = Semantica(config=config)
# Top-level orchestrator
sem = Semantica(config_path="config.yaml")
sem.initialize()
# Plugin registry: register custom components under a name
# Plugin registry: register custom components
registry = PluginRegistry()
registry.register_plugin("my_ingestor", MyCustomIngestor, version="1.0.0")
registry.register("my_ingestor", MyCustomIngestor)
# Config management
config = ConfigManager(config_path="config.yaml")
batch = config.get("processing.batch_size", default=32)
```
**Components:** `Semantica`, `PluginRegistry`, `ConfigManager`, `Config`, `LifecycleManager`, `HealthStatus`, `MethodRegistry`
**Components:** `Semantica`, `PluginRegistry`, `ConfigManager`, `LifecycleManager`, `HealthMonitor`, `Config`
### Utils
@@ -556,13 +532,11 @@ from semantica.semantic_extract import NERExtractor, RelationExtractor
from semantica.kg import GraphBuilder
sources = FileIngestor().ingest("data/")
text = DocumentParser().parse(sources[0].path)["full_text"]
ner = NERExtractor(method="llm", provider="groq", llm_model="llama-3.3-70b-versatile")
rel = RelationExtractor(method="llm", provider="groq", llm_model="llama-3.3-70b-versatile")
entities = ner.extract(text)
relationships = rel.extract(text, entities=entities)
parsed = DocumentParser().parse(sources[0])
entities = NERExtractor(method="llm", llm_provider=llm).extract(parsed)
relationships = RelationExtractor(method="llm", llm_provider=llm).extract(parsed, entities=entities)
graph = GraphBuilder(merge_entities=True).build(
{"entities": entities, "relationships": relationships}
entities=entities, relationships=relationships
)
```
@@ -581,20 +555,16 @@ from semantica.vector_store import VectorStore
context = AgentContext(
vector_store=VectorStore(backend="faiss", dimension=768),
knowledge_graph=ContextGraph(advanced_analytics=True),
graph_expansion=True,
)
context.load_graph("company_kg.json")
# store() extracts entities and populates the graph + vector index
context.store([{"content": "Steve Wozniak co-founded Apple with Steve Jobs."}])
# retrieve() blends vector similarity with multi-hop graph traversal
results = context.retrieve(
result = context.query(
"What companies did Apple alumni found?",
use_graph=True,
expand_graph=True,
mode="graphrag",
reasoning=True,
)
for r in results:
print(f"[{r['score']:.3f}] {r['content']} (source: {r['source']})")
for claim in result.claims:
print(f"{claim.text} {claim.source_node}")
```
**Best for:** question-answering systems, RAG with source attribution, research assistants
@@ -636,22 +606,18 @@ precedents = context.find_precedents("model selection", limit=5)
```python
from semantica.ingest import FileIngestor
from semantica.parse import DocumentParser
from semantica.semantic_extract import NERExtractor
from semantica.kg import GraphBuilder
from semantica.provenance import ProvenanceManager
from semantica.export import RDFExporter
sources = FileIngestor().ingest("records/")
ner = NERExtractor(method="llm", provider="groq", llm_model="llama-3.3-70b-versatile")
entities = ner.extract(DocumentParser().parse(sources[0].path)["full_text"])
graph = GraphBuilder(merge_entities=True).build({"entities": entities, "relationships": []})
entities = NERExtractor(method="llm", llm_provider=llm).extract(sources)
graph = GraphBuilder(merge_entities=True).build(entities=entities, relationships=[])
prov = ProvenanceManager()
prov.track_entity("entity_id", source="records/filing.pdf", metadata={"extractor": "llm"})
lineage = prov.get_lineage("entity_id")
lineage = prov.get_entity_lineage("entity_id")
RDFExporter().export(graph, file_path="audit.ttl", format="turtle")
RDFExporter(include_provenance=True).export(graph, file_path="audit.ttl", format="turtle")
```
**Best for:** HIPAA, SOX, GDPR, FDA 21 CFR Part 11 deployments
@@ -666,25 +632,18 @@ RDFExporter().export(graph, file_path="audit.ttl", format="turtle")
from semantica.ingest import WebIngestor
from semantica.normalize import TextNormalizer
from semantica.semantic_extract import NERExtractor, RelationExtractor
from semantica.graph_store import GraphStore
from semantica.kg import GraphBuilder
from semantica.graph_store import Neo4jStore
ingestor = WebIngestor()
pages = WebIngestor(max_depth=2).ingest("https://example.com")
normalizer = TextNormalizer()
ner = NERExtractor(method="pattern")
rel = RelationExtractor(method="pattern")
store = Neo4jStore(uri="bolt://localhost:7687", user="neo4j", password="password")
# The generic GraphStore wrapper exposes the add_nodes/add_edges interface
# GraphBuilder persists through; a raw Neo4jStore does not
store = GraphStore(backend="neo4j", uri="bolt://localhost:7687", user="neo4j", password="password")
builder = GraphBuilder(merge_entities=True, graph_store=store)
for url in ["https://example.com/a", "https://example.com/b"]:
page = ingestor.ingest_url(url) # WebContent, has .text
for page in pages:
text = normalizer.normalize_text(page.text)
entities = ner.extract(text)
relationships = rel.extract(text, entities=entities)
builder.build({"entities": entities, "relationships": relationships})
entities = NERExtractor().extract(text)
relationships = RelationExtractor().extract(text, entities=entities)
store.add_nodes(entities)
store.add_edges(relationships)
```
**Best for:** competitive intelligence, news monitoring, research aggregation
@@ -733,8 +692,8 @@ versioner.create_snapshot(kg, "2024-Q1", author="user@example.com", description=
| [vector_store](/reference/vector_store) | Vector database | `VectorStore` |
| [graph_store](/reference/graph_store) | Graph database | `GraphStore` |
| [triplet_store](/reference/triplet_store) | RDF triple store | `TripletStore` |
| [deduplication](/reference/deduplication) | Entity resolution | `DuplicateDetector`, `EntityMerger`, `ClusterBuilder`, `MergeStrategyManager` |
| [conflicts](/reference/conflicts) | Conflict resolution | `ConflictDetector`, `ConflictResolver`, `SourceTracker` |
| [deduplication](/reference/deduplication) | Entity resolution | `EntityResolver`, `DuplicateDetector`, `ClusterBuilder`, `MergeStrategyManager` |
| [conflicts](/reference/conflicts) | Conflict resolution | `ConflictDetector` |
| [context](/reference/context) | Agent context & decisions | `AgentContext`, `ContextGraph` |
| [provenance](/reference/provenance) | W3C PROV-O lineage | `ProvenanceManager` |
| [change_management](/reference/change_management) | Version control | `TemporalVersionManager` |
@@ -744,8 +703,8 @@ versioner.create_snapshot(kg, "2024-Q1", author="user@example.com", description=
| [explorer](/reference/explorer) | Knowledge Explorer UI | `semantica-explorer --graph <file>` |
| [llms](/reference/llms) | LLM providers | `Groq`, `OpenAI`, `create_provider` |
| [mcp_server](/reference/mcp_server) | MCP stdio server | `python -m semantica.mcp_server` |
| [seed](/reference/seed) | KG bootstrapping from structured sources | `SeedDataManager` |
| [evals](/reference/evals) | Decision-intelligence evaluation | `evaluate`, `list_evaluators`, `EvalSummary` |
| [seed](/reference/seed) | KG bootstrapping from structured sources | `SeedManager` |
| [evals](/reference/evals) | Quality evaluation | `KGEvaluator`, `ExtractionEvaluator`, `PipelineEvaluator`, `RegressionTracker` |
| [core](/reference/core) | Base classes & registry | `Semantica`, `ConfigManager`, `PluginRegistry`, `LifecycleManager` |
| [utils](/reference/utils) | Shared utilities | `helpers`, `validators` |
+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-5",
model="anthropic/claude-sonnet-4-20250514",
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-5", api_key=os.getenv("ANTHROPIC_API_KEY"))
"smart": LiteLLM(model="anthropic/claude-sonnet-4-20250514", 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-7", api_key=os.getenv("ANTHROPIC_API_KEY"))
llm = LiteLLM(model="anthropic/claude-opus-4-5", 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-sonnet-4-5-20250929-v1:0")
llm = LiteLLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2: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-7", api_key=os.getenv("ANTHROPIC_API_KEY")),
"Anthropic": LiteLLM(model="anthropic/claude-opus-4-5", 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-sonnet-4-5-20250929-v1:0"),
"Bedrock": LiteLLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2: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-5")` | Highest reasoning capability |
| **Complex Analysis** | `LiteLLM("anthropic/claude-sonnet-4-20250514")` | Highest reasoning capability |
| **High Volume/Cost** | `LiteLLM("deepseek/deepseek-chat")` | Lowest cost per token |
### Error Handling
+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)")
# → a list of binding dicts: [{"Z": "bob"}, {"Z": "charlie"}, {"Z": "dave"}] (order not guaranteed)
# → [{"Z": "bob"}, {"Z": "charlie"}, {"Z": "dave"}]
# Clear and start over
datalog.clear()
+1 -1
View File
@@ -9,7 +9,7 @@
"lint": "eslint .",
"preview": "vite preview",
"test:graph-store": "node --test tests/graphStore.multi-edge.test.mjs",
"test:graph-workspace": "node --import tsx --test tests/markdownContentViewer.test.ts tests/graphSceneState.display.test.ts tests/temporalLifecycle.test.ts tests/deterministicExplorerRendering.test.ts tests/smallGraphLayout.test.ts tests/realtimeGraphAttributes.test.ts tests/ontologyEditorModel.test.ts",
"test:graph-workspace": "node --import tsx --test tests/markdownContentViewer.test.ts tests/graphSceneState.display.test.ts tests/temporalLifecycle.test.ts tests/deterministicExplorerRendering.test.ts tests/smallGraphLayout.test.ts tests/realtimeGraphAttributes.test.ts",
"test:deterministic-e2e": "node --import tsx --test tests/deterministicExplorerRendering.e2e.ts",
"test:plugin-registry": "node --import tsx --test tests/pluginRegistry.temporal.test.mjs"
},
+1 -13
View File
@@ -93,18 +93,6 @@ const navItems: NavItem[] = [
{ id: 'ontology-hub', label: 'Ontology Hub', hint: 'Schema governance, registry, and vocabulary management', icon: GitMerge },
];
function readInitialWorkspace(): WorkspaceId {
try {
const params = new URLSearchParams(window.location.search);
if (params.has("ontologyTab") || params.has("ontologyEntity")) {
return "ontology-hub";
}
} catch {
// Default to the welcome screen when URL state is unavailable.
}
return "welcome";
}
const shellStyles = `
:root {
--app-bg: #07111f;
@@ -1785,7 +1773,7 @@ function WelcomeScreen({
}
export default function App() {
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceId>(readInitialWorkspace);
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceId>('welcome');
const [exploreView, setExploreView] = useState<ExploreView>('graph');
const [analyzeView, setAnalyzeView] = useState<AnalyzeView>('reasoning');
const [enrichView, setEnrichView] = useState<EnrichView>('import');
@@ -8,10 +8,8 @@ import {
useNodesState,
useEdgesState,
MarkerType,
Handle,
Position,
} from "@xyflow/react";
import type { Connection, Edge, Node, ReactFlowInstance } from "@xyflow/react";
import type { Connection, Edge, Node } from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import {
Plus,
@@ -24,20 +22,10 @@ import {
Pencil,
Trash2,
} from "lucide-react";
import { loadOntologyEntityOwner, loadOntologyGraph } from "./api";
import type { OntologyGraphEdge, OntologyGraphNode } from "./api";
import {
classifyNodeType,
inferOntologyUri,
isEditableEntityType,
ONTOLOGY_MINIMAP_THEME,
} from "./ontologyEditorModel";
import type { EditorEntityType, RegistryEntry } from "./ontologyEditorModel";
type OntologyNodeData = {
label?: string;
type?: string;
entityType?: EditorEntityType;
};
type OntologyNode = Node<OntologyNodeData>;
@@ -46,57 +34,12 @@ type OntologyEdge = Edge<Record<string, unknown>>;
const nodeTypes = {
classNode: ({ data }: { data: OntologyNodeData }) => (
<div style={classNodeStyle}>
<Handle type="target" position={Position.Left} style={handleStyle} />
<div style={classNodeHeader}>{data.label}</div>
<div style={classNodeSub}>{data.type}</div>
<Handle type="source" position={Position.Right} style={handleStyle} />
</div>
),
};
const handleStyle: React.CSSProperties = {
width: 8,
height: 8,
border: "1px solid rgba(235, 243, 255, 0.8)",
background: "#4aa3ff",
};
const ontologyFlowThemeCss = `
.ontology-editor-flow .react-flow__controls {
overflow: hidden;
border: 1px solid rgba(127, 208, 255, 0.2);
border-radius: 9px;
background: rgba(6, 13, 26, 0.96);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.38);
}
.ontology-editor-flow .react-flow__controls-button {
width: 30px;
height: 30px;
background: transparent;
border-bottom-color: rgba(127, 208, 255, 0.14);
color: #8fa8c6;
transition: color 140ms ease, background 140ms ease;
}
.ontology-editor-flow .react-flow__controls-button:hover {
background: rgba(74, 163, 255, 0.14);
color: #ebf3ff;
}
.ontology-editor-flow .react-flow__controls-button:focus-visible {
position: relative;
z-index: 1;
outline: 2px solid #7fd0ff;
outline-offset: -2px;
}
.ontology-editor-flow .react-flow__controls-button:disabled {
background: rgba(3, 9, 18, 0.32);
color: #40566f;
}
`;
const classNodeStyle: React.CSSProperties = {
padding: "12px 16px",
borderRadius: "8px",
@@ -136,95 +79,17 @@ interface DraftDiff {
annotation_changes: Record<string, Record<string, any>>;
}
function requestedEntityUri(): string {
try {
return new URLSearchParams(window.location.search).get("ontologyEntity") || "";
} catch {
return "";
}
}
function nodeLabel(node: OntologyGraphNode): string {
const explicit = String(node.content || node.properties?.["rdfs:label"] || "").trim();
if (explicit && explicit !== node.id) {
return explicit;
}
const trimmed = node.id.replace(/[/#]+$/, "");
return trimmed.split("#").pop() || trimmed.split("/").pop() || node.id;
}
function classifyEditorNode(node: OntologyGraphNode): OntologyNodeData["entityType"] {
return classifyNodeType(node.type);
}
function layoutEditorNodes(inputNodes: OntologyNode[]): OntologyNode[] {
const properties = inputNodes.filter((node) => node.data.entityType === "property");
const targets = inputNodes.filter((node) => (
node.data.entityType === "class" || node.data.entityType === "external"
));
const context = inputNodes.filter((node) => (
node.data.entityType !== "property"
&& node.data.entityType !== "class"
&& node.data.entityType !== "external"
));
const height = Math.max(360, Math.max(properties.length, targets.length) * 180);
const positions = new Map<string, { x: number; y: number }>();
properties.forEach((node, index) => {
positions.set(node.id, { x: 0, y: ((index + 1) * height) / (properties.length + 1) });
});
targets.forEach((node, index) => {
positions.set(node.id, { x: 600, y: ((index + 1) * height) / (targets.length + 1) });
});
context.forEach((node, index) => {
positions.set(node.id, { x: 300 + index * 220, y: height + 120 });
});
return inputNodes.map((node) => ({
...node,
position: positions.get(node.id) || node.position,
}));
}
function buildEditorElements(apiNodes: OntologyGraphNode[], apiEdges: OntologyGraphEdge[]) {
const sortedNodes = [...apiNodes].sort((left, right) => {
const typeDelta = left.type.localeCompare(right.type);
return typeDelta || left.id.localeCompare(right.id);
});
const nodes = layoutEditorNodes(sortedNodes.map((node) => ({
id: node.id,
type: "classNode",
position: { x: 0, y: 0 },
data: {
label: nodeLabel(node),
type: node.type,
entityType: classifyEditorNode(node),
},
})));
const edges: OntologyEdge[] = apiEdges.map((edge, index) => ({
id: edge.id || `${edge.source}:${edge.type}:${edge.target}:${index}`,
source: edge.source,
target: edge.target,
label: edge.type,
type: "default",
markerEnd: { type: MarkerType.ArrowClosed },
style: { stroke: "rgba(127, 208, 255, 0.72)", strokeWidth: 1.5 },
labelStyle: { fill: "#c8dcf5", fontSize: 11, fontWeight: 600 },
labelBgStyle: { fill: "#07111f", fillOpacity: 0.9 },
}));
return { nodes, edges };
interface RegistryEntry {
uri: string;
name: string;
}
export function OntologyEditor() {
const [nodes, setNodes, onNodesChange] = useNodesState<OntologyNode>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<OntologyEdge>([]);
const [selectedElement, setSelectedElement] = useState<OntologyNode | OntologyEdge | null>(null);
const hasDetailPanel = selectedElement !== null;
const [registry, setRegistry] = useState<RegistryEntry[]>([]);
const [ontologyUri, setOntologyUri] = useState<string>("");
const [flowInstance, setFlowInstance] = useState<ReactFlowInstance<OntologyNode, OntologyEdge> | null>(null);
const [isLoadingGraph, setIsLoadingGraph] = useState(false);
const [graphError, setGraphError] = useState("");
const [draftDiff, setDraftDiff] = useState<DraftDiff>({
added_classes: [],
removed_classes: [],
@@ -243,18 +108,12 @@ export function OntologyEditor() {
useEffect(() => {
let cancelled = false;
const requested = requestedEntityUri();
Promise.all([
fetch("/api/ontology/registry").then((response) => (response.ok ? response.json() : [])),
requested
? loadOntologyEntityOwner(requested).catch(() => undefined)
: Promise.resolve(undefined),
])
.then(([entries, explicitOwner]: [RegistryEntry[], string | undefined]) => {
fetch("/api/ontology/registry")
.then((response) => (response.ok ? response.json() : []))
.then((entries: RegistryEntry[]) => {
if (cancelled) return;
setRegistry(entries);
const inferredOntology = inferOntologyUri(entries, requested, explicitOwner);
setOntologyUri((current) => current || inferredOntology || entries[0]?.uri || "");
setOntologyUri((current) => current || entries[0]?.uri || "");
})
.catch((error) => {
console.error("Failed to load ontology registry:", error);
@@ -264,47 +123,6 @@ export function OntologyEditor() {
};
}, []);
useEffect(() => {
if (!ontologyUri) {
setNodes([]);
setEdges([]);
setSelectedElement(null);
return;
}
const controller = new AbortController();
setIsLoadingGraph(true);
setGraphError("");
loadOntologyGraph(ontologyUri, controller.signal)
.then((payload) => {
const elements = buildEditorElements(payload.nodes, payload.edges);
setNodes(elements.nodes);
setEdges(elements.edges);
const requested = requestedEntityUri();
setSelectedElement(elements.nodes.find((node) => node.id === requested) || null);
})
.catch((error) => {
if (controller.signal.aborted) return;
setNodes([]);
setEdges([]);
setSelectedElement(null);
setGraphError(error instanceof Error ? error.message : "Failed to load ontology graph");
})
.finally(() => {
if (!controller.signal.aborted) setIsLoadingGraph(false);
});
return () => controller.abort();
}, [ontologyUri, setEdges, setNodes]);
useEffect(() => {
if (!flowInstance || nodes.length === 0) return;
const frame = window.requestAnimationFrame(() => {
void flowInstance.fitView({ padding: 0.22, duration: 320, maxZoom: 1.25 });
});
return () => window.cancelAnimationFrame(frame);
}, [flowInstance, hasDetailPanel, nodes.length, ontologyUri]);
const onConnect = useCallback(
(params: Connection) => setEdges((eds) => addEdge({ ...params, markerEnd: { type: MarkerType.ArrowClosed } }, eds)),
[setEdges]
@@ -316,7 +134,7 @@ export function OntologyEditor() {
id: newId,
type: "classNode",
position: { x: Math.random() * 400, y: Math.random() * 300 },
data: { label: "NewClass", type: "owl:Class", entityType: "class" },
data: { label: "NewClass", type: "owl:Class" },
};
setNodes((nds) => [...nds, newNode]);
setDraftDiff((prev) => ({
@@ -352,7 +170,7 @@ export function OntologyEditor() {
id: newId,
type: "classNode",
position: { x: Math.random() * 400, y: Math.random() * 300 },
data: { label: "NewIndividual", type: "owl:NamedIndividual", entityType: "external" },
data: { label: "NewIndividual", type: "owl:NamedIndividual" },
};
setNodes((nds) => [...nds, newNode]);
}, [setNodes]);
@@ -372,21 +190,13 @@ export function OntologyEditor() {
}, []);
const autoLayout = useCallback(() => {
setNodes(layoutEditorNodes(nodes));
const layoutNodes = nodes.map((node, index) => ({
...node,
position: { x: (index % 4) * 200, y: Math.floor(index / 4) * 150 },
}));
setNodes(layoutNodes);
}, [nodes, setNodes]);
const selectNode = useCallback((node: OntologyNode) => {
setSelectedElement(node);
try {
const params = new URLSearchParams(window.location.search);
params.set("ontologyTab", "editor");
params.set("ontologyEntity", node.id);
window.history.replaceState(null, "", `?${params.toString()}`);
} catch {
// URL state is optional; the editor selection still works without it.
}
}, []);
const saveDraft = useCallback(async () => {
if (!ontologyUri) {
alert("Please select an ontology first");
@@ -437,11 +247,12 @@ export function OntologyEditor() {
...prev,
removed_properties: [...prev.removed_properties, target.id],
}));
} else if (isEditableEntityType(target.data.entityType)) {
} else {
setNodes((nds) => nds.filter((n) => n.id !== target.id));
setDraftDiff((prev) => target.data.entityType === "property"
? { ...prev, removed_properties: [...prev.removed_properties, target.id] }
: { ...prev, removed_classes: [...prev.removed_classes, target.id] });
setDraftDiff((prev) => ({
...prev,
removed_classes: [...prev.removed_classes, target.id],
}));
}
setSelectedElement(null);
}
@@ -450,21 +261,16 @@ export function OntologyEditor() {
const renameSelected = useCallback(() => {
const target = showContext?.element ?? selectedElement;
if (target && !("source" in target) && isEditableEntityType(target.data.entityType)) {
if (target && !("source" in target)) {
const newLabel = prompt("Enter new name:", String(target.data.label ?? ""));
if (newLabel) {
setNodes((nds) =>
nds.map((n) => (n.id === target.id ? { ...n, data: { ...n.data, label: newLabel } } : n))
);
setDraftDiff((prev) => target.data.entityType === "property"
? {
...prev,
modified_properties: { ...prev.modified_properties, [target.id]: { label: newLabel } },
}
: {
...prev,
modified_classes: { ...prev.modified_classes, [target.id]: { label: newLabel } },
});
setDraftDiff((prev) => ({
...prev,
modified_classes: { ...prev.modified_classes, [target.id]: { label: newLabel } },
}));
}
}
setShowContext(null);
@@ -533,10 +339,11 @@ export function OntologyEditor() {
};
const detailPanelStyle: React.CSSProperties = {
flex: "0 0 320px",
position: "absolute",
right: 0,
top: 0,
bottom: 0,
width: "320px",
minWidth: "320px",
boxSizing: "border-box",
background: "rgba(9, 19, 34, 0.95)",
borderLeft: "1px solid rgba(140, 192, 255, 0.12)",
padding: "20px",
@@ -546,24 +353,11 @@ export function OntologyEditor() {
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%", background: "#07111f" }}>
<style>{ontologyFlowThemeCss}</style>
<div style={toolbarStyle}>
<select
aria-label="Active ontology"
value={ontologyUri}
onChange={(event) => {
setOntologyUri(event.target.value);
setSelectedElement(null);
try {
// Drop the previous ontology's entity from the URL, or a reload
// would resolve the stale ID and jump back to that ontology.
const params = new URLSearchParams(window.location.search);
params.delete("ontologyEntity");
window.history.replaceState(null, "", `?${params.toString()}`);
} catch {
// URL state is optional; switching ontologies still works.
}
}}
onChange={(event) => setOntologyUri(event.target.value)}
style={selectStyle}
>
<option value="">Select ontology...</option>
@@ -604,75 +398,43 @@ export function OntologyEditor() {
</button>
</div>
<div style={{ display: "flex", flex: 1, minHeight: 0, minWidth: 0 }}>
<div style={{ flex: 1, minHeight: 0, minWidth: 0, position: "relative" }}>
<ReactFlow
className="ontology-editor-flow"
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={setFlowInstance}
onNodeClick={(_, node) => selectNode(node)}
onEdgeClick={(_, edge) => setSelectedElement(edge)}
onNodeContextMenu={handleNodeContextMenu}
onEdgeContextMenu={handleEdgeContextMenu}
nodeTypes={nodeTypes}
fitView
style={{ background: "#07111f" }}
>
<Background color="#1a2d3d" gap={20} />
<Controls />
<MiniMap {...ONTOLOGY_MINIMAP_THEME} />
</ReactFlow>
<div style={{ flex: 1, position: "relative" }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={(_, node) => setSelectedElement(node)}
onEdgeClick={(_, edge) => setSelectedElement(edge)}
onNodeContextMenu={handleNodeContextMenu}
onEdgeContextMenu={handleEdgeContextMenu}
nodeTypes={nodeTypes}
fitView
style={{ background: "#07111f" }}
>
<Background color="#1a2d3d" gap={20} />
<Controls />
<MiniMap nodeColor="#4aa3ff" maskColor="rgba(0,0,0,0.6)" />
</ReactFlow>
{isLoadingGraph && (
<div style={canvasMessageStyle}>Loading ontology structure</div>
)}
{!isLoadingGraph && graphError && (
<div style={{ ...canvasMessageStyle, color: "#ff9a8d" }}>{graphError}</div>
)}
{!isLoadingGraph && !graphError && ontologyUri && nodes.length === 0 && (
<div style={canvasMessageStyle}>This ontology has no editable classes or properties.</div>
)}
{showContext && (
<div style={{ ...contextMenuStyle, left: showContext.x, top: showContext.y }}>
{"source" in showContext.element || isEditableEntityType(showContext.element.data.entityType) ? (
<>
{!("source" in showContext.element) && (
<div style={contextItemStyle} onClick={renameSelected}>
<Pencil size={14} />
Rename
</div>
)}
<div style={contextItemStyle} onClick={deleteSelected}>
<Trash2 size={14} />
Delete
</div>
</>
) : (
<div style={{ ...contextItemStyle, cursor: "default", color: "#8fa8c6" }}>
This term is read-only
</div>
)}
{showContext && (
<div style={{ ...contextMenuStyle, left: showContext.x, top: showContext.y }}>
<div style={contextItemStyle} onClick={renameSelected}>
<Pencil size={14} />
Rename
</div>
)}
</div>
<div style={contextItemStyle} onClick={deleteSelected}>
<Trash2 size={14} />
Delete
</div>
</div>
)}
{selectedElement && (
<div style={detailPanelStyle}>
<h3 style={{ margin: "0 0 16px", color: "#ebf3ff", fontSize: "16px" }}>
{"source" in selectedElement
? "Relationship Details"
: selectedElement.data.entityType === "property"
? "Property Details"
: selectedElement.data.entityType === "ontology"
? "Ontology Details"
: selectedElement.data.entityType === "external"
? "External Term Details"
: "Class Details"}
{"source" in selectedElement ? "Property Details" : "Class Details"}
</h3>
<div style={{ marginBottom: "12px" }}>
<label style={{ display: "block", color: "#8fa8c6", fontSize: "12px", marginBottom: "4px" }}>
@@ -691,9 +453,7 @@ export function OntologyEditor() {
<input
type="text"
value={String(selectedElement.data.label ?? "")}
readOnly={!isEditableEntityType(selectedElement.data.entityType)}
onChange={(e) => {
if (!isEditableEntityType(selectedElement.data.entityType)) return;
setNodes((nds) =>
nds.map((n) =>
n.id === selectedElement.id
@@ -703,19 +463,10 @@ export function OntologyEditor() {
);
setDraftDiff((prev) => ({
...prev,
...(selectedElement.data.entityType === "property"
? {
modified_properties: {
...prev.modified_properties,
[selectedElement.id]: { label: e.target.value },
},
}
: {
modified_classes: {
...prev.modified_classes,
[selectedElement.id]: { label: e.target.value },
},
}),
modified_classes: {
...prev.modified_classes,
[selectedElement.id]: { label: e.target.value },
},
}));
}}
style={{
@@ -745,17 +496,3 @@ export function OntologyEditor() {
</div>
);
}
const canvasMessageStyle: React.CSSProperties = {
position: "absolute",
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
padding: "10px 14px",
borderRadius: "8px",
border: "1px solid rgba(127, 208, 255, 0.18)",
background: "rgba(3, 9, 18, 0.9)",
color: "#8fa8c6",
fontSize: "13px",
pointerEvents: "none",
};
@@ -9,32 +9,6 @@ import type {
ShaclValidationResponse,
} from "./types";
export type OntologyGraphNode = {
id: string;
type: string;
content?: string;
properties?: Record<string, unknown>;
};
export type OntologyGraphEdge = {
id?: string;
source: string;
target: string;
type: string;
weight?: number;
properties?: Record<string, unknown>;
};
export type OntologyGraphResponse = {
uri: string;
nodes: OntologyGraphNode[];
edges: OntologyGraphEdge[];
};
export type OntologyEntityOwner = {
source_ontology?: string;
};
async function parseResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
let detail = `Request failed with status ${response.status}`;
@@ -57,18 +31,6 @@ export async function loadOntologyRegistry(): Promise<OntologyEntry[]> {
return parseResponse<OntologyEntry[]>(await fetch("/api/ontology/registry"));
}
export async function loadOntologyGraph(uri: string, signal?: AbortSignal): Promise<OntologyGraphResponse> {
return parseResponse<OntologyGraphResponse>(
await fetch(`/api/ontology/graph?uri=${encodeURIComponent(uri)}`, { signal }),
);
}
export async function loadOntologyEntityOwner(uri: string): Promise<string | undefined> {
const response = await fetch(`/api/ontology/entity/${encodeURIComponent(uri)}`);
if (!response.ok) return undefined;
return (await response.json() as OntologyEntityOwner).source_ontology;
}
export async function loadAlignments(uri?: string): Promise<OntologyAlignment[]> {
const query = uri ? `?uri=${encodeURIComponent(uri)}` : "";
return parseResponse<OntologyAlignment[]>(await fetch(`/api/ontology/alignments${query}`));
@@ -38,7 +38,6 @@ function readTabParam(): OntologyHubTab {
const params = new URLSearchParams(window.location.search);
const raw = params.get(TAB_PARAM);
if (raw && TABS.some((t) => t.id === raw)) return raw as OntologyHubTab;
if (params.get("ontologyEntity")) return "editor";
} catch {
// ignore
}
@@ -117,3 +116,4 @@ export function OntologyWorkspace({ onJumpToGraphNode }: OntologyWorkspaceProps)
</div>
);
}
@@ -1,70 +0,0 @@
export type EditorEntityType = "ontology" | "class" | "property" | "external";
export type RegistryEntry = {
uri: string;
name: string;
};
export const ONTOLOGY_MINIMAP_THEME = {
bgColor: "#0b1625",
maskColor: "rgba(7, 17, 31, 0.72)",
maskStrokeColor: "#5faeff",
maskStrokeWidth: 2,
nodeColor: "#2d7fd3",
nodeStrokeColor: "#9acbff",
nodeStrokeWidth: 1,
style: {
border: "1px solid #29435c",
borderRadius: 6,
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.32)",
},
} as const;
// The backend emits node types in compact (owl:Class) or full IRI
// (http://www.w3.org/2002/07/owl#Class) form; classification must accept both.
const FULL_IRI_PREFIXES: Array<[string, string]> = [
["http://www.w3.org/2002/07/owl#", "owl:"],
["http://www.w3.org/2000/01/rdf-schema#", "rdfs:"],
["http://www.w3.org/2004/02/skos/core#", "skos:"],
];
export function compactNodeType(type: string): string {
for (const [iri, prefix] of FULL_IRI_PREFIXES) {
if (type.startsWith(iri)) {
return `${prefix}${type.slice(iri.length)}`;
}
}
return type;
}
export function classifyNodeType(rawType: string): EditorEntityType {
const type = compactNodeType(rawType);
if (type === "owl:Ontology") return "ontology";
if (type === "owl:Class" || type === "rdfs:Class") return "class";
if (type.includes("Property")) return "property";
return "external";
}
function ownsByNamespace(entityUri: string, ontologyUri: string): boolean {
const stem = ontologyUri.replace(/[/#]+$/, "");
return entityUri === ontologyUri
|| entityUri.startsWith(`${stem}#`)
|| entityUri.startsWith(`${stem}/`);
}
export function inferOntologyUri(
entries: RegistryEntry[],
entityUri: string,
explicitOwner?: string,
): string | undefined {
if (explicitOwner && entries.some((entry) => entry.uri === explicitOwner)) {
return explicitOwner;
}
return [...entries]
.filter((entry) => ownsByNamespace(entityUri, entry.uri))
.sort((left, right) => right.uri.length - left.uri.length)[0]?.uri;
}
export function isEditableEntityType(entityType?: EditorEntityType): boolean {
return entityType === "class" || entityType === "property";
}
@@ -1,66 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
classifyNodeType,
compactNodeType,
inferOntologyUri,
isEditableEntityType,
ONTOLOGY_MINIMAP_THEME,
} from "../src/workspaces/OntologyWorkspace/ontologyEditorModel";
const registry = [
{ uri: "https://example.test/foo", name: "Foo" },
{ uri: "https://example.test/foo/nested", name: "Nested" },
];
test("ontology inference requires a URI delimiter and prefers the closest namespace", () => {
assert.equal(inferOntologyUri(registry, "https://example.test/foobar/Class"), undefined);
assert.equal(
inferOntologyUri(registry, "https://example.test/foo/nested#Class"),
"https://example.test/foo/nested",
);
});
test("explicit scheme ownership wins when an entity uses another namespace", () => {
assert.equal(
inferOntologyUri(registry, "https://vocabulary.test/Class", "https://example.test/foo"),
"https://example.test/foo",
);
});
test("only draft-supported class and property nodes are editable", () => {
assert.equal(isEditableEntityType("class"), true);
assert.equal(isEditableEntityType("property"), true);
assert.equal(isEditableEntityType("ontology"), false);
assert.equal(isEditableEntityType("external"), false);
});
test("the ontology minimap has an explicit dark, high-contrast theme", () => {
assert.equal(ONTOLOGY_MINIMAP_THEME.bgColor, "#0b1625");
assert.equal(ONTOLOGY_MINIMAP_THEME.maskStrokeColor, "#5faeff");
assert.equal(ONTOLOGY_MINIMAP_THEME.nodeStrokeColor, "#9acbff");
assert.match(ONTOLOGY_MINIMAP_THEME.style.border, /#29435c/);
});
test("node types classify identically in compact and full IRI form", () => {
const cases: Array<[string, string, string]> = [
["owl:Ontology", "http://www.w3.org/2002/07/owl#Ontology", "ontology"],
["owl:Class", "http://www.w3.org/2002/07/owl#Class", "class"],
["rdfs:Class", "http://www.w3.org/2000/01/rdf-schema#Class", "class"],
["owl:ObjectProperty", "http://www.w3.org/2002/07/owl#ObjectProperty", "property"],
["owl:DatatypeProperty", "http://www.w3.org/2002/07/owl#DatatypeProperty", "property"],
["owl:AnnotationProperty", "http://www.w3.org/2002/07/owl#AnnotationProperty", "property"],
];
for (const [compact, fullIri, expected] of cases) {
assert.equal(classifyNodeType(compact), expected, compact);
assert.equal(classifyNodeType(fullIri), expected, fullIri);
}
assert.equal(classifyNodeType("owl:NamedIndividual"), "external");
assert.equal(classifyNodeType("http://www.w3.org/2004/02/skos/core#Concept"), "external");
});
test("compactNodeType leaves unknown namespaces untouched", () => {
assert.equal(compactNodeType("https://example.org/custom#Thing"), "https://example.org/custom#Thing");
assert.equal(compactNodeType("owl:Class"), "owl:Class");
});
+7 -36
View File
@@ -47,11 +47,7 @@ dependencies = [
"numpy>=2.0.2",
"pandas>=1.3.0",
"scipy>=1.13.1",
# 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'",
"scikit-learn>=1.7.2",
"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,
@@ -70,49 +66,24 @@ dependencies = [
"seaborn>=0.13.2",
"plotly>=6.8.0",
"ipywidgets>=8.0.0",
# 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'",
"requests>=2.34.2",
"GitPython>=3.1.58",
# 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'",
"chardet>=7.4.3",
"protobuf>=5.29.1,<8.0",
# 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'",
"grpcio>=1.81.1",
"beautifulsoup4>=4.15.0",
"lxml>=6.1.1",
"python-docx>=1.2.0",
"openpyxl>=3.1.5",
# 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'",
"pillow>=12.2.0",
"librosa>=0.9.0",
"opencv-python>=4.13.0.92",
"faiss-cpu>=1.7.0",
"fastembed>=0.2.0",
# 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'",
"onnxruntime>=1.20.1",
"tokenizers>=0.15.0",
"pydantic>=2.13.4",
# 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'",
"click>=8.4.2",
"rich>=12.5.0",
"tqdm>=4.68.3",
"pyyaml>=6.0",
+6 -160
View File
@@ -234,12 +234,6 @@ class EntityDetailResponse(BaseModel):
properties: Dict[str, Any] = Field(default_factory=dict)
class OntologyGraphResponse(BaseModel):
uri: str
nodes: List[Dict[str, Any]] = Field(default_factory=list)
edges: List[Dict[str, Any]] = Field(default_factory=list)
class SKOSScheme(BaseModel):
uri: str
title: str
@@ -670,7 +664,6 @@ def _convert_ontology_to_graph(ontology_dict: Dict[str, Any]) -> Tuple[List[Dict
"rdfs:label": cls.get("label", cls.get("name", "")),
"rdfs:comment": cls.get("description", ""),
"uri": cls_uri,
"scheme_uri": ontology_uri,
},
}
nodes.append(node)
@@ -687,21 +680,14 @@ def _convert_ontology_to_graph(ontology_dict: Dict[str, Any]) -> Tuple[List[Dict
# Add property nodes and edges
for prop in ontology_dict.get("properties", []):
prop_uri = prop.get("uri", f"temp:prop:{uuid.uuid4().hex[:12]}")
property_type = {
"object": "owl:ObjectProperty",
"data": "owl:DatatypeProperty",
"datatype": "owl:DatatypeProperty",
"annotation": "owl:AnnotationProperty",
}.get(str(prop.get("type", "object")).lower(), "owl:ObjectProperty")
node = {
"id": prop_uri,
"type": property_type,
"type": f"owl:{prop.get('type', 'Object').title()}Property",
"content": prop.get("name", prop.get("label", "")),
"properties": {
"rdfs:label": prop.get("label", prop.get("name", "")),
"rdfs:comment": prop.get("description", ""),
"uri": prop_uri,
"scheme_uri": ontology_uri,
},
}
nodes.append(node)
@@ -762,38 +748,14 @@ def _node_source_ontology(node: Dict[str, Any]) -> Optional[str]:
)
def _node_belongs_to_ontology(
node: Dict[str, Any],
ontology_uri: str,
known_ontology_uris: Optional[set[str]] = None,
) -> bool:
def _node_belongs_to_ontology(node: Dict[str, Any], ontology_uri: str) -> bool:
nid = node.get("id", "")
if nid == ontology_uri:
return True
owner = _node_source_ontology(node)
if owner:
return owner == ontology_uri
if known_ontology_uris:
namespace_owners = [
candidate
for candidate in known_ontology_uris
if nid == candidate
or nid.startswith(
(candidate.rstrip("#/") + "#", candidate.rstrip("#/") + "/")
)
]
if namespace_owners and max(namespace_owners, key=len) != ontology_uri:
return False
if _node_source_ontology(node) == ontology_uri:
return True
stem = ontology_uri.rstrip("#/")
if not nid.startswith((stem + "#", stem + "/")):
return False
# Prefix ownership only extends to names minted directly in the
# ontology's namespace (<stem>#Term or <stem>/Term). Any further
# delimiter marks a nested vocabulary (<stem>/child#Term,
# <stem>/child/Term), which must not be absorbed into the parent
# until it is registered or carries an explicit owner.
local_name = nid[len(stem) + 1 :]
return "#" not in local_name and "/" not in local_name
return nid.startswith((stem + "#", stem + "/"))
def _is_ontology_entity(node: Dict[str, Any]) -> bool:
@@ -1259,8 +1221,7 @@ def _parse_rdf_sync(content: bytes, fmt: str) -> tuple:
metadata.setdefault("description", str(obj))
break
synthetic_uri = "uri" not in metadata
if synthetic_uri:
if "uri" not in metadata:
metadata["uri"] = f"urn:semantica:onto:{uuid.uuid4().hex[:8]}"
metadata.setdefault("name", metadata["uri"].rsplit("/", 1)[-1].rsplit("#", 1)[-1] or "Unnamed")
metadata["triple_count"] = len(g)
@@ -1307,20 +1268,6 @@ def _parse_rdf_sync(content: bytes, fmt: str) -> tuple:
"weight": 1.0,
})
if synthetic_uri:
# No owl:Ontology / skos:ConceptScheme declaration exists, so the
# synthetic registry URI shares no namespace with any node. Ownership
# must be recorded explicitly, and the editor needs a matching graph
# node, or the registered ontology resolves to an empty core and 404s.
for node in nodes:
node["properties"].setdefault("scheme_uri", metadata["uri"])
nodes.append({
"id": metadata["uri"],
"type": "owl:Ontology",
"content": metadata["name"],
"properties": {"rdfs:label": metadata["name"], "uri": metadata["uri"]},
})
return nodes, edges, metadata
@@ -1821,107 +1768,6 @@ async def search_entities(
return results
@router.get("/graph", response_model=OntologyGraphResponse)
async def get_ontology_graph(
request: Request,
uri: str = Query(..., min_length=1),
session: GraphSession = Depends(get_session),
):
"""Return the editable schema subgraph for one registered ontology."""
registry = _get_registry(request)
ontology_nodes: List[Dict[str, Any]] = []
for node_type in _ONTOLOGY_TYPES:
nodes, _ = await asyncio.to_thread(
session.get_nodes, node_type=node_type, skip=0, limit=2**63 - 1
)
ontology_nodes.extend(nodes)
known_ontology_uris = set(registry) | {
str(node.get("id", "")) for node in ontology_nodes if node.get("id")
}
if uri not in known_ontology_uris:
raise HTTPException(status_code=404, detail="Ontology not found in registry.")
schema_types = _CLASS_TYPES | _PROPERTY_TYPES | _CONCEPT_TYPES | _ONTOLOGY_TYPES
candidates_by_id: Dict[str, Dict[str, Any]] = {}
for node_type in schema_types:
nodes, _ = await asyncio.to_thread(
session.get_nodes, node_type=node_type, skip=0, limit=2**63 - 1
)
candidates_by_id.update(
(str(node.get("id", "")), node) for node in nodes if node.get("id")
)
core_node_ids = {
str(node.get("id", ""))
for node in candidates_by_id.values()
if _node_belongs_to_ontology(node, uri, known_ontology_uris)
}
if not core_node_ids:
raise HTTPException(status_code=404, detail="Ontology graph not found.")
structure_edge_types = {
"rdf:type",
"rdfs:subClassOf",
"rdfs:domain",
"rdfs:range",
"owl:disjointWith",
"owl:equivalentClass",
"owl:equivalentProperty",
"owl:inverseOf",
"skos:broader",
"skos:narrower",
"skos:related",
}
selected_edges: List[Dict[str, Any]] = []
for edge_type in structure_edge_types:
edges, _ = await asyncio.to_thread(
session.get_edges,
edge_type=edge_type,
skip=0,
limit=2**63 - 1,
)
# Keep only edges whose source is a core node: the requested ontology
# may reference outward (e.g. rdfs:range to an external vocabulary),
# but an unrelated ontology's property pointing at a core class must
# not leak inward.
selected_edges.extend(
edge for edge in edges
if str(edge.get("source", "")) in core_node_ids
)
if (
len(core_node_ids) > _MAX_ANALYSIS_NODES
or len(selected_edges) > _MAX_ANALYSIS_NODES
):
raise HTTPException(
status_code=413,
detail=(
"Ontology editor graph exceeds the maximum size "
f"({_MAX_ANALYSIS_NODES} nodes or edges)."
),
)
selected_node_ids = set(core_node_ids)
for edge in selected_edges:
selected_node_ids.add(str(edge.get("source", "")))
selected_node_ids.add(str(edge.get("target", "")))
selected_nodes = [candidates_by_id[node_id] for node_id in core_node_ids]
for node_id in selected_node_ids - core_node_ids:
external = await asyncio.to_thread(session.get_node, node_id)
if external is not None:
selected_nodes.append(external)
selected_nodes.sort(key=lambda node: str(node.get("id", "")))
selected_edges.sort(
key=lambda edge: (
str(edge.get("source", "")),
str(edge.get("type", "")),
str(edge.get("target", "")),
str(edge.get("id", "")),
)
)
return OntologyGraphResponse(uri=uri, nodes=selected_nodes, edges=selected_edges)
@router.get("/entity/{entity_uri:path}", response_model=EntityDetailResponse)
async def get_entity_detail(
entity_uri: str,
+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-5")
>>> llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
>>> 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-5", "groq/llama-3.1-8b-instant")
Model format: "provider/model-name" (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514", "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-5")
>>> llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
>>> 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-5",
Examples: "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514",
"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.)
+1 -24
View File
@@ -37,29 +37,6 @@ from .naming_conventions import NamingConventions
from .relationship_utils import build_entity_aliases, resolve_relationship_endpoint_type
# Top-level entity keys that describe structure or provenance rather than
# business attributes. GraphBuilder and EntityMerger attach these to entity
# dicts (relationships list, nested properties/metadata maps, merge history),
# so they must not be inferred as datatype properties. Each key mirrors what
# the framework actually writes to a merged entity top level
# (see MergeStrategyManager._merge_entities merged_entity dict and GraphBuilder).
_CONTROL_FIELDS = frozenset(
{
"id",
"type",
"entity_type",
"text",
"label",
"confidence",
"properties",
"relationships",
"metadata",
"merged_from",
"merge_strategy",
}
)
class PropertyGenerator:
"""
Property generation engine for ontologies.
@@ -370,7 +347,7 @@ class PropertyGenerator:
for entity in entities:
for key, value in entity.items():
if key in _CONTROL_FIELDS:
if key in ["id", "type", "entity_type", "text", "label", "confidence"]:
continue
# Infer type
+3 -203
View File
@@ -12,11 +12,7 @@ from semantica.context.context_graph import ContextGraph
pytest.importorskip("fastapi")
from semantica.explorer.app import create_app # noqa: E402
from semantica.explorer.routes.ontology import ( # noqa: E402
OntologyEntry,
_convert_ontology_to_graph,
_node_belongs_to_ontology,
)
from semantica.explorer.routes.ontology import OntologyEntry # noqa: E402
from semantica.explorer.session import GraphSession # noqa: E402
from starlette.testclient import TestClient # noqa: E402
@@ -135,181 +131,6 @@ def test_health_returns_dimensions_and_issues(client):
assert isinstance(payload["issues"], list)
def test_ontology_graph_returns_editable_schema_nodes_and_edges(client):
response = client.get(
"/api/ontology/graph",
params={"uri": "http://example.org/onto-a"},
)
assert response.status_code == 200
payload = response.json()
node_ids = {node["id"] for node in payload["nodes"]}
assert "http://example.org/onto-a" in node_ids
assert "http://example.org/onto-a#Person" in node_ids
assert "http://example.org/onto-a#name" in node_ids
assert any(
edge["source"] == "http://example.org/onto-a#name"
and edge["target"] == "http://example.org/onto-a#Person"
and edge["type"] == "rdfs:domain"
for edge in payload["edges"]
)
def test_ontology_graph_rejects_unregistered_namespace(client):
response = client.get(
"/api/ontology/graph",
params={"uri": "http://example.org"},
)
assert response.status_code == 404
def test_ontology_graph_excludes_separately_registered_nested_ontology(client):
graph = client.app.state.session.graph
nested = "http://example.org/onto-a/nested"
nested_class = f"{nested}#PrivateClass"
graph.add_node(nested, node_type="owl:Ontology", content="Nested Ontology")
graph.add_node(nested_class, node_type="owl:Class", content="Private Class")
response = client.get(
"/api/ontology/graph",
params={"uri": "http://example.org/onto-a"},
)
assert response.status_code == 200
node_ids = {node["id"] for node in response.json()["nodes"]}
assert nested not in node_ids
assert nested_class not in node_ids
def test_ontology_graph_prefers_explicit_ownership_over_uri_namespace(client):
graph = client.app.state.session.graph
explicit_member = "http://unrelated.example/Person"
graph.add_node(
explicit_member,
node_type="owl:Class",
content="Explicit Member",
scheme_uri="http://example.org/onto-a",
)
response = client.get(
"/api/ontology/graph",
params={"uri": "http://example.org/onto-a"},
)
assert response.status_code == 200
assert explicit_member in {node["id"] for node in response.json()["nodes"]}
def test_ontology_graph_excludes_inward_edges_from_other_ontologies(client):
graph = client.app.state.session.graph
foreign_prop = "http://example.org/onto-b#recordOf"
graph.add_node(
foreign_prop,
node_type="owl:ObjectProperty",
content="record of",
scheme_uri="http://example.org/onto-b",
)
# onto-b's property points its domain at onto-a's class: an inward
# reference that must not pull the foreign property into onto-a's graph.
graph.add_edge(foreign_prop, "http://example.org/onto-a#Person", edge_type="rdfs:domain")
response = client.get(
"/api/ontology/graph",
params={"uri": "http://example.org/onto-a"},
)
assert response.status_code == 200
payload = response.json()
assert foreign_prop not in {node["id"] for node in payload["nodes"]}
assert all(edge["source"] != foreign_prop for edge in payload["edges"])
def test_ontology_graph_excludes_unregistered_nested_namespace(client):
graph = client.app.state.session.graph
nested_class = "http://example.org/onto-a/vocab#Term"
graph.add_node(nested_class, node_type="owl:Class", content="Nested Term")
response = client.get(
"/api/ontology/graph",
params={"uri": "http://example.org/onto-a"},
)
assert response.status_code == 200
assert nested_class not in {node["id"] for node in response.json()["nodes"]}
def test_node_belongs_to_ontology_nested_namespace_matrix():
parent = "http://example.org/onto-a"
child = "http://example.org/onto-a/nested"
def node(node_id):
return {"id": node_id, "properties": {}}
assert _node_belongs_to_ontology(node(f"{parent}#Person"), parent, {parent})
assert _node_belongs_to_ontology(node(f"{parent}/Person"), parent, {parent})
# An unregistered nested namespace is not absorbed into the parent,
# whether fragment-based or path-based
assert not _node_belongs_to_ontology(node(f"{child}#Term"), parent, {parent})
assert not _node_belongs_to_ontology(node(f"{child}/Term"), parent, {parent})
# Once registered, the nested namespace owns its nodes
assert not _node_belongs_to_ontology(node(f"{child}#Term"), parent, {parent, child})
assert _node_belongs_to_ontology(node(f"{child}#Term"), child, {parent, child})
assert _node_belongs_to_ontology(node(f"{child}/Term"), child, {parent, child})
def test_load_fallback_import_without_declaration_is_editable(client):
turtle = """
@prefix ex: <http://data.example.org/people#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:Employee a rdfs:Class ;
rdfs:label "Employee" .
ex:manager a rdf:Property ;
rdfs:label "manager" .
"""
with patch(
"semantica.ingest.ontology_ingestor.OntologyIngestor.ingest_ontology",
side_effect=RuntimeError("force fallback parser"),
):
loaded = client.post(
"/api/ontology/load",
json={"content": turtle, "format": "turtle"},
)
assert loaded.status_code == 200
uri = loaded.json()["uri"]
assert uri.startswith("urn:semantica:onto:")
response = client.get("/api/ontology/graph", params={"uri": uri})
assert response.status_code == 200
payload = response.json()
node_ids = {node["id"] for node in payload["nodes"]}
assert uri in node_ids
assert "http://data.example.org/people#Employee" in node_ids
def test_ontology_graph_ignores_unrelated_data_when_enforcing_size_limit(client):
graph = client.app.state.session.graph
for index in range(5_001):
graph.add_node(
f"urn:unrelated:{index}",
node_type="owl:Class",
content="Unrelated",
scheme_uri="http://example.org/onto-b",
)
response = client.get(
"/api/ontology/graph",
params={"uri": "http://example.org/onto-a"},
)
assert response.status_code == 200
assert "http://example.org/onto-a#Person" in {
node["id"] for node in response.json()["nodes"]
}
def test_shacl_generate_and_shapes(client):
response = client.post(
"/api/ontology/shacl/generate",
@@ -875,29 +696,6 @@ def test_ontology_load_does_not_swallow_422_from_ingestor_success_path(client):
fallback_parse.assert_not_called()
def test_convert_ontology_uses_standard_property_types_and_scheme_uri():
ontology_uri = "http://example.org/onto"
nodes, _ = _convert_ontology_to_graph(
{
"uri": ontology_uri,
"name": "Example Ontology",
"classes": [
{"uri": f"{ontology_uri}#Person", "name": "Person"},
],
"properties": [
{"uri": f"{ontology_uri}#name", "name": "name", "type": "data"},
{"uri": f"{ontology_uri}#knows", "name": "knows", "type": "object"},
],
}
)
by_id = {node["id"]: node for node in nodes}
assert by_id[f"{ontology_uri}#Person"]["properties"]["scheme_uri"] == ontology_uri
assert by_id[f"{ontology_uri}#name"]["type"] == "owl:DatatypeProperty"
assert by_id[f"{ontology_uri}#knows"]["type"] == "owl:ObjectProperty"
assert by_id[f"{ontology_uri}#name"]["properties"]["scheme_uri"] == ontology_uri
# ---------------------------------------------------------------------------
# refresh_ontology — single combined add_nodes_and_edges() coverage (#775)
# ---------------------------------------------------------------------------
@@ -991,3 +789,5 @@ def test_refresh_ontology_missing_source_url_returns_422(client):
response = client.post(f"/api/ontology/{encoded_uri}/refresh")
assert response.status_code == 422
assert "source url" in response.json()["detail"].lower()
@@ -1,94 +0,0 @@
"""Framework/control entity keys must not be inferred as datatype properties.
The _CONTROL_FIELDS skip set mirrors exactly what the framework writes to a
merged entity's top level (see MergeStrategyManager._merge_entities): no extra
guesses, so business attributes that merely share a common name (e.g. source)
keep getting inferred.
"""
from semantica.deduplication.merge_strategy import MergeStrategyManager
from semantica.ontology.property_generator import PropertyGenerator
def _merged_entity():
"""Run a real merge so the entity carries the framework's actual top-level keys."""
manager = MergeStrategyManager(default_strategy="keep_most_complete")
result = manager.merge_entities(
[
{"id": "b1", "name": "Hangzhou Branch", "type": "ORG", "employee_count": 120},
{"id": "b2", "name": "Hangzhou Branch", "type": "ORG"},
]
)
return result.merged_entity
def test_framework_fields_not_inferred_as_data_properties():
entity = _merged_entity()
classes = [{"name": "Organization", "metadata": {"inferred_from": "ORG"}}]
properties = PropertyGenerator().infer_properties([entity], [], classes)
names = {p["name"] for p in properties}
for framed in (
"properties",
"relationships",
"metadata",
"merged_from",
"merge_strategy",
):
assert framed not in names, f"framework field {framed} leaked as a property"
def test_business_attributes_still_inferred():
entity = _merged_entity()
classes = [{"name": "Organization", "metadata": {"inferred_from": "ORG"}}]
properties = PropertyGenerator().infer_properties([entity], [], classes)
names = {p["name"] for p in properties}
assert "name" in names
assert "metadata" not in names
def test_source_field_still_inferred_as_business_attribute():
"""A top-level 'source' is a business attribute, not a framework field."""
entity = {
"id": "b1",
"name": "Hangzhou Branch",
"type": "ORG",
"source": "doc-42",
}
classes = [{"name": "Organization", "metadata": {"inferred_from": "ORG"}}]
properties = PropertyGenerator().infer_properties([entity], [], classes)
names = {p["name"] for p in properties}
assert "source" in names
def test_unmerged_graphbuilder_entities_infer_business_attributes():
"""Flat entities from GraphBuilder (merge_entities=False) must not lose business
attributes through _CONTROL_FIELDS: name and domain-specific fields must be
inferred, and none of the framework keys should appear in the output."""
from semantica.kg.graph_builder import GraphBuilder
builder = GraphBuilder(merge_entities=False, resolve_conflicts=False)
graph = builder.build(
{
"entities": [
{"id": "c1", "name": "Chengdu Plant", "type": "ORG", "headcount": 300},
{"id": "c2", "name": "Wuhan Plant", "type": "ORG", "headcount": 450},
],
"relationships": [],
}
)
entities = graph["entities"]
classes = [{"name": "Organization", "metadata": {"inferred_from": "ORG"}}]
properties = PropertyGenerator().infer_properties(entities, [], classes)
names = {p["name"] for p in properties}
assert "name" in names, "name must be inferred from flat GraphBuilder entities"
assert "headcount" in names, "domain business attribute must be inferred"
for framed in ("properties", "relationships", "metadata", "merged_from", "merge_strategy"):
assert framed not in names, f"framework field {framed!r} must not appear"