mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-04 04:01:07 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
071f78c77c | ||
|
|
301c4636d7 |
+183
-4
@@ -1,9 +1,14 @@
|
||||
/* ============================================================
|
||||
SEMANTICA DOCS — DESIGN SYSTEM
|
||||
SEMANTICA DOCS — PREMIUM DESIGN SYSTEM
|
||||
Dark-first (#080C10 bg, #10B981 emerald accent)
|
||||
Minimal, static styling — no decorative motion.
|
||||
============================================================ */
|
||||
|
||||
/* ── Keyframes ─────────────────────────────────────────────── */
|
||||
@keyframes pageFadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Global ─────────────────────────────────────────────────── */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
@@ -24,7 +29,16 @@ html {
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(16, 185, 129, 0.4); }
|
||||
|
||||
/* ── Focus rings (accessibility — kept) ─────────────────────── */
|
||||
/* ── Page entrance ──────────────────────────────────────────── */
|
||||
main,
|
||||
article,
|
||||
[class*="content-area"],
|
||||
[class*="ContentArea"],
|
||||
[class*="prose"] {
|
||||
animation: pageFadeIn 0.35s ease both;
|
||||
}
|
||||
|
||||
/* ── Focus rings ─────────────────────────────────────────────── */
|
||||
*:focus-visible {
|
||||
outline: 2px solid rgba(16, 185, 129, 0.55) !important;
|
||||
outline-offset: 3px !important;
|
||||
@@ -45,7 +59,7 @@ h1::after {
|
||||
left: 0;
|
||||
width: 44px;
|
||||
height: 2px;
|
||||
background: #10B981;
|
||||
background: linear-gradient(90deg, #10B981 0%, transparent 100%);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
@@ -57,6 +71,9 @@ article a,
|
||||
[class*="prose"] a {
|
||||
text-decoration-color: rgba(16, 185, 129, 0.35);
|
||||
text-underline-offset: 3px;
|
||||
transition:
|
||||
text-decoration-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
article a:hover,
|
||||
@@ -72,6 +89,14 @@ blockquote {
|
||||
padding: 0.9rem 1.2rem !important;
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.68) !important;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease !important;
|
||||
}
|
||||
|
||||
blockquote:hover {
|
||||
border-left-color: rgba(16, 185, 129, 0.65) !important;
|
||||
background: rgba(16, 185, 129, 0.07) !important;
|
||||
}
|
||||
|
||||
/* ── HR / Divider ────────────────────────────────────────────── */
|
||||
@@ -98,11 +123,165 @@ table thead th {
|
||||
border-bottom: 1px solid rgba(16, 185, 129, 0.18) !important;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
transition: background-color 0.15s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
table tbody tr:hover {
|
||||
background-color: rgba(16, 185, 129, 0.06) !important;
|
||||
}
|
||||
|
||||
table tbody tr:hover td {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
/* ── CODE BLOCKS ─────────────────────────────────────────────── */
|
||||
pre,
|
||||
[class*="codeblock"],
|
||||
[class*="code-group"],
|
||||
[class*="CodeBlock"],
|
||||
[data-rehype-pretty-code-fragment] {
|
||||
transition:
|
||||
box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
}
|
||||
|
||||
pre:hover,
|
||||
[class*="codeblock"]:hover,
|
||||
[class*="CodeBlock"]:hover,
|
||||
[data-rehype-pretty-code-fragment]:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(16, 185, 129, 0.18),
|
||||
0 2px 12px rgba(16, 185, 129, 0.06),
|
||||
0 8px 32px rgba(0, 0, 0, 0.2) !important;
|
||||
border-color: rgba(16, 185, 129, 0.2) !important;
|
||||
}
|
||||
|
||||
/* ── CARDS ───────────────────────────────────────────────────── */
|
||||
[class*="card"],
|
||||
[class*="Card"],
|
||||
[data-card],
|
||||
.group\/card {
|
||||
transition:
|
||||
transform 0.22s ease,
|
||||
box-shadow 0.22s ease,
|
||||
border-color 0.22s ease !important;
|
||||
}
|
||||
|
||||
[class*="card"]:hover,
|
||||
[class*="Card"]:hover,
|
||||
[data-card]:hover,
|
||||
.group\/card:hover {
|
||||
transform: translateY(-3px) !important;
|
||||
box-shadow:
|
||||
0 8px 28px rgba(0, 0, 0, 0.18),
|
||||
0 0 0 1px rgba(16, 185, 129, 0.22) !important;
|
||||
border-color: rgba(16, 185, 129, 0.28) !important;
|
||||
}
|
||||
|
||||
/* ── CALLOUTS / ADMONITIONS ──────────────────────────────────── */
|
||||
[class*="callout"],
|
||||
[class*="Callout"],
|
||||
[class*="admonition"] {
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease !important;
|
||||
}
|
||||
|
||||
[class*="callout"]:hover,
|
||||
[class*="Callout"]:hover,
|
||||
[class*="admonition"]:hover {
|
||||
box-shadow: 0 2px 16px rgba(16, 185, 129, 0.08) !important;
|
||||
border-color: rgba(16, 185, 129, 0.35) !important;
|
||||
}
|
||||
|
||||
/* ── STEPS ───────────────────────────────────────────────────── */
|
||||
[class*="step"],
|
||||
[class*="Step"] {
|
||||
transition: background-color 0.15s ease !important;
|
||||
}
|
||||
|
||||
[class*="step"]:hover,
|
||||
[class*="Step"]:hover {
|
||||
background-color: rgba(16, 185, 129, 0.04) !important;
|
||||
}
|
||||
|
||||
/* ── INLINE CODE ─────────────────────────────────────────────── */
|
||||
:not(pre) > code {
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease !important;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
:not(pre) > code:hover {
|
||||
background-color: rgba(16, 185, 129, 0.16) !important;
|
||||
}
|
||||
|
||||
/* ── NAVIGATION / SIDEBAR ────────────────────────────────────── */
|
||||
nav a,
|
||||
[class*="sidebar"] a,
|
||||
[class*="Sidebar"] a {
|
||||
transition: color 0.15s ease !important;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
nav a::after,
|
||||
[class*="sidebar"] a::after,
|
||||
[class*="Sidebar"] a::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: #10B981;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
nav a:hover::after,
|
||||
[class*="sidebar"] a:hover::after,
|
||||
[class*="Sidebar"] a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── TEXT / LIST ITEMS ───────────────────────────────────────── */
|
||||
ul > li,
|
||||
ol > li {
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.12s ease;
|
||||
}
|
||||
|
||||
ul > li:hover,
|
||||
ol > li:hover {
|
||||
background-color: rgba(16, 185, 129, 0.04);
|
||||
}
|
||||
|
||||
/* ── PRIMARY BUTTON / CTA ────────────────────────────────────── */
|
||||
button[class*="primary"],
|
||||
a[class*="primary"],
|
||||
[class*="btn-primary"],
|
||||
[class*="ButtonPrimary"] {
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease !important;
|
||||
}
|
||||
|
||||
button[class*="primary"]:hover,
|
||||
a[class*="primary"]:hover,
|
||||
[class*="btn-primary"]:hover,
|
||||
[class*="ButtonPrimary"]:hover {
|
||||
box-shadow: 0 0 22px rgba(16, 185, 129, 0.28) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
}
|
||||
|
||||
/* ── HIDE THEME TOGGLE ───────────────────────────────────────── */
|
||||
|
||||
+10
-10
@@ -8,16 +8,16 @@ icon: "book-open"
|
||||
New here? Start with [Getting Started](/getting-started) for hands-on examples, then return here for deeper understanding.
|
||||
</Info>
|
||||
|
||||
Semantica transforms unstructured data (documents, web pages, reports, databases) into **knowledge graphs**: structured representations that AI systems can query, reason about, and trace back to sources.
|
||||
Semantica transforms unstructured data: documents, web pages, reports, databases: into **knowledge graphs**: structured representations that AI systems can query, reason about, and trace back to sources.
|
||||
|
||||
At its core, Semantica adds a context and semantic layer on top of your existing AI stack. It doesn't replace LangChain, LlamaIndex, or your LLM provider. It makes their outputs grounded, traceable, and auditable.
|
||||
At its core, Semantica adds a **context and accountability layer** on top of your existing AI stack. It doesn't replace LangChain, LlamaIndex, or your LLM provider: it makes their outputs **grounded**, **traceable**, and **auditable**.
|
||||
|
||||
- **Context Layer.** Knowledge graphs, GraphRAG retrieval, semantic embeddings, and temporal intelligence ground every LLM response in structured, queryable facts.
|
||||
- **Accountability Layer.** Provenance tracking, decision intelligence, conflict detection, and W3C PROV-O compliance make every claim in your AI stack auditable and explainable.
|
||||
- **Extension Layer.** `PluginRegistry` and `MethodRegistry` let you replace or augment any component (ingestors, extractors, reasoning engines, backends) without changing framework code.
|
||||
- **Context Layer** — Knowledge graphs, GraphRAG retrieval, semantic embeddings, and temporal intelligence ground every LLM response in structured, queryable facts.
|
||||
- **Accountability Layer** — Provenance tracking, decision intelligence, conflict detection, and W3C PROV-O compliance make every claim in your AI stack auditable and explainable.
|
||||
- **Extension Layer** — `PluginRegistry` and `MethodRegistry` let you replace or augment any component: ingestors, extractors, reasoning engines, backends: without changing framework code.
|
||||
|
||||
<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. In short, Semantica explains and audits *what the AI system did*, not the foundation model's private internal reasoning.
|
||||
**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. In short, Semantica explains and audits *what the AI system did*, not the foundation model's private internal reasoning.
|
||||
</Warning>
|
||||
|
||||
## Knowledge Graphs
|
||||
@@ -30,7 +30,7 @@ The foundation of everything in Semantica. A knowledge graph stores information
|
||||
- **Edges (relationships)**: `works_for`, `located_in`, `founded_by`
|
||||
- **Properties**: name, date, confidence score, source URL
|
||||
|
||||
This structure makes knowledge searchable, connectable, and queryable. Critically, it's explainable: every answer can be traced back to the facts and relationships that produced it.
|
||||
This structure makes knowledge **searchable**, **connectable**, **queryable**, and: critically: **explainable**: every answer can be traced back to the facts and relationships that produced it.
|
||||
|
||||
|
||||
## Entity Extraction (NER)
|
||||
@@ -513,6 +513,6 @@ Semantica is designed for extension. Any component: ingestor, extractor, graph b
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
- [Quickstart Tutorial](/quickstart): build a full pipeline with code.
|
||||
- [Modules Guide](/modules): every module explained with examples.
|
||||
- [API Reference](/reference/context): complete technical reference.
|
||||
- [Quickstart Tutorial](/quickstart) — Build a full pipeline with code.
|
||||
- [Modules Guide](/modules) — Every module explained with examples.
|
||||
- [API Reference](/reference/context) — Complete technical reference.
|
||||
|
||||
+29
-30
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://mintlify.com/docs.json",
|
||||
"theme": "mint",
|
||||
"name": "Semantica",
|
||||
"description": "The Context and Semantic Layer for AI in High-Stakes Domains — Context Graphs · Decision Intelligence · Full Provenance",
|
||||
"description": "The Accountability and Context Layer for AI — Context Graphs · Decision Intelligence · Full Provenance",
|
||||
"colors": {
|
||||
"primary": "#10B981",
|
||||
"light": "#10B981",
|
||||
@@ -43,7 +43,7 @@
|
||||
"raiseIssue": true
|
||||
},
|
||||
"metadata": {
|
||||
"og:title": "Semantica — Context & Semantic Layer for AI in High-Stakes Domains",
|
||||
"og:title": "Semantica — Accountability & Context Layer for AI",
|
||||
"og:description": "Build explainable, auditable knowledge graphs with full provenance. Open source. MIT licensed.",
|
||||
"og:image": "/assets/img/semantica-logo.png",
|
||||
"twitter:card": "summary_large_image",
|
||||
@@ -121,23 +121,6 @@
|
||||
"pages": [
|
||||
"vector_stores/pgvector"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "FAQ",
|
||||
"pages": [
|
||||
"faq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Community",
|
||||
"pages": [
|
||||
"community",
|
||||
"community-projects",
|
||||
"contributing-guide",
|
||||
"governance",
|
||||
"citation",
|
||||
"project-license"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -184,17 +167,7 @@
|
||||
"guides/policy-engine",
|
||||
"guides/visualization",
|
||||
"guides/distance-intelligence",
|
||||
"guides/graph-analytics"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "API Reference",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Context & Intelligence",
|
||||
"pages": [
|
||||
"guides/graph-analytics",
|
||||
"reference/context",
|
||||
"reference/kg",
|
||||
"reference/temporal",
|
||||
@@ -263,6 +236,32 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "FAQ",
|
||||
"groups": [
|
||||
{
|
||||
"group": "FAQ",
|
||||
"pages": [
|
||||
"faq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Community",
|
||||
"pages": [
|
||||
"community",
|
||||
"community-projects",
|
||||
"contributing-guide",
|
||||
"governance",
|
||||
"citation",
|
||||
"project-license"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Changelog",
|
||||
"href": "https://github.com/semantica-agi/semantica/releases"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+37
-50
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "GraphRAG: Graph-Augmented Retrieval"
|
||||
title: "GraphRAG — Graph-Augmented Retrieval"
|
||||
description: "Go beyond vector search: retrieve facts, trace reasoning paths, and ground LLM responses in your knowledge graph."
|
||||
---
|
||||
|
||||
GraphRAG combines vector similarity with knowledge graph traversal so retrieval finds structurally connected facts, not just text that sounds related. When a `ContextGraph` is attached to `AgentContext`, every retrieval call automatically blends semantic search with multi-hop graph expansion, and `query_with_reasoning()` returns an auditable reasoning path alongside the LLM answer.
|
||||
GraphRAG combines vector similarity with knowledge graph traversal so retrieval finds structurally connected facts, not just text that sounds related. When a `ContextGraph` is attached to `AgentContext`, every retrieval call automatically blends semantic search with multi-hop graph expansion — and `query_with_reasoning()` returns an auditable reasoning path alongside the LLM answer.
|
||||
|
||||
## What Is GraphRAG?
|
||||
|
||||
@@ -11,7 +11,7 @@ GraphRAG (Graph-Augmented Retrieval-Augmented Generation) enhances traditional R
|
||||
|
||||
**GraphRAG vs. traditional vector-only RAG:** Vector RAG finds documents similar to your query text. GraphRAG finds documents similar to your query AND documents connected to those through entity relationships, even if they don't mention your query terms directly.
|
||||
|
||||
**The role of graph traversal:** Starting from entities found in vector-similar documents, GraphRAG expands outward through relationship edges to discover related facts. This reveals connections that pure text similarity would miss, like finding that a threat actor targets healthcare by following the path: Actor → Tool → Victim Organization → Industry Sector.
|
||||
**The role of graph traversal:** Starting from entities found in vector-similar documents, GraphRAG expands outward through relationship edges to discover related facts. This reveals connections that pure text similarity would miss — like finding that a threat actor targets healthcare by following the path: Actor → Tool → Victim Organization → Industry Sector.
|
||||
|
||||
## Why Use GraphRAG?
|
||||
|
||||
@@ -96,7 +96,7 @@ context = AgentContext(
|
||||
)
|
||||
```
|
||||
|
||||
Now ingest your documents. `store()` with `extract_entities=True` runs the full extraction pipeline internally (Named Entity Recognition, relation extraction, and entity linking) and populates both the vector index and the graph simultaneously:
|
||||
Now ingest your documents. `store()` with `extract_entities=True` runs the full extraction pipeline internally — Named Entity Recognition (NER), relation extraction, and entity linking — and populates both the vector index and the graph simultaneously:
|
||||
|
||||
```python
|
||||
intel_documents = [
|
||||
@@ -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.
|
||||
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:
|
||||
@@ -169,25 +169,17 @@ Notice the top results: while pure vector search might rank connected facts lowe
|
||||
When you know specifically which entity you want to anchor the traversal to, pass `anchor_node`:
|
||||
|
||||
```python
|
||||
# Anchor on APT29 explicitly: proximity scores are calculated from this node
|
||||
# Anchor on APT29 explicitly — proximity scores are calculated from this node
|
||||
apt29_intel = context.retrieve(
|
||||
"C2 infrastructure beaconing patterns",
|
||||
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.
|
||||
@@ -205,7 +197,7 @@ result = context.query_with_reasoning(
|
||||
max_hops=3,
|
||||
)
|
||||
|
||||
# The LLM answer, grounded in graph-retrieved context, not training memory
|
||||
# The LLM answer — grounded in graph-retrieved context, not training memory
|
||||
print(result["response"])
|
||||
|
||||
# The multi-hop trace: APT29 → deployed → HAMMERTOSS → observed_on → LifeCare → ...
|
||||
@@ -221,7 +213,7 @@ for src in result["sources"]:
|
||||
print(" [{:.3f}] {}".format(src["score"], src["content"][:80]))
|
||||
```
|
||||
|
||||
The `reasoning_path` field is what separates GraphRAG from a black-box LLM call. When an analyst asks "how do you know APT29 targeted healthcare?", you can show them the exact traversal the system made across your own documents, not a claim the model generated from training data.
|
||||
The `reasoning_path` field is what separates GraphRAG from a black-box LLM call. When an analyst asks "how do you know APT29 targeted healthcare?", you can show them the exact traversal the system made across your own documents — not a claim the model generated from training data.
|
||||
|
||||
The full return structure from `query_with_reasoning()`:
|
||||
|
||||
@@ -240,11 +232,11 @@ The full return structure from `query_with_reasoning()`:
|
||||
|
||||
<Tabs>
|
||||
|
||||
<Tab title="Defense: CTI/Threat">
|
||||
<Tab title="Defense — CTI/Threat">
|
||||
|
||||
Multi-INT intelligence fusion: OSINT threat feeds, NVD CVE data, and HUMINT summaries ingested into a single graph, then queried with multi-hop reasoning to trace C2 infrastructure chains and attribute campaigns to specific actors.
|
||||
|
||||
In classified environments the graph can be partitioned by data handling caveat: each `AgentContext` operates over the subset of documents cleared for the querying user. The `reasoning_path` output doubles as a sanitisable audit trail for downgraded reporting.
|
||||
In classified environments the graph can be partitioned by data handling caveat — each `AgentContext` operates over the subset of documents cleared for the querying user. The `reasoning_path` output doubles as a sanitisable audit trail for downgraded reporting.
|
||||
|
||||
```python
|
||||
from semantica.context import AgentContext, ContextGraph
|
||||
@@ -308,11 +300,11 @@ proximate = context.retrieve(
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Security: SOC/Incident">
|
||||
<Tab title="Security — SOC/Incident">
|
||||
|
||||
Security operations: real-time alert triage against a graph containing hosts, CVEs, user accounts, runbooks, and historical incidents. GraphRAG retrieves the relevant runbook and similar past incidents in a single call, reducing mean-time-to-respond.
|
||||
|
||||
The `decision_tracking=True` flag records every triage query as an auditable decision, with the full context that was provided to the LLM. That's essential for post-incident review and SOC metrics.
|
||||
The `decision_tracking=True` flag records every triage query as an auditable decision, with the full context that was provided to the LLM — essential for post-incident review and SOC metrics.
|
||||
|
||||
```python
|
||||
from semantica.context import AgentContext, ContextGraph
|
||||
@@ -377,7 +369,7 @@ for inc in similar:
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Life Science: Clinical/Pharma">
|
||||
<Tab title="Life Science — Clinical/Pharma">
|
||||
|
||||
Clinical decision support: FDA drug labels, clinical guidelines, and trial summaries ingested into a graph where drug-enzyme-metabolite-interaction chains become traversable paths. A three-hop query (drug → enzyme → metabolite → contraindication) surfaces interaction risks that no single document would make explicit.
|
||||
|
||||
@@ -451,7 +443,7 @@ contra_chain = clinical_context.retrieve(
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Banking: Risk/Compliance">
|
||||
<Tab title="Banking — Risk/Compliance">
|
||||
|
||||
Regulatory compliance: Basel III (CRE20), BCBS 239, SR 11-7, and EBA IRRBB guidelines ingested as a graph where regulation articles cross-reference each other as edges. Multi-hop queries traverse those cross-references automatically, so a question about commercial real estate RWA pulls the relevant CRE20 paragraphs and the BCBS 239 data quality requirements that govern their calculation in a single call.
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
@@ -509,7 +496,7 @@ print(answer["response"])
|
||||
print("Regulatory sources cited: {}".format(answer["num_sources"]))
|
||||
print("Confidence: {:.1%}".format(answer["confidence"]))
|
||||
|
||||
# The reasoning path is the audit log: show it to the regulator
|
||||
# The reasoning path is the audit log — show it to the regulator
|
||||
print("\n--- Reasoning Path (audit log) ---")
|
||||
print(answer["reasoning_path"])
|
||||
```
|
||||
@@ -537,18 +524,18 @@ The `hybrid_alpha` parameter set in the `AgentContext` constructor establishes a
|
||||
When targeting a specific `anchor_node`, you can apply `proximity_weight` in `retrieve()` to dynamically blend structural distance from the anchor into the final score:
|
||||
|
||||
```python
|
||||
# Anchor node provided: let vector semantics lead, graph proximity only slightly boosts
|
||||
# Anchor node provided — let vector semantics lead, graph proximity only slightly boosts
|
||||
results = context.retrieve(
|
||||
query, use_graph=True, anchor_node="APT29", proximity_weight=0.2
|
||||
)
|
||||
|
||||
# Known-entity tracing: topology drives the retrieval
|
||||
# Known-entity tracing — topology drives the retrieval
|
||||
results = context.retrieve(
|
||||
query, use_graph=True, anchor_node="APT29", proximity_weight=0.8
|
||||
)
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -589,9 +576,9 @@ The vector search and graph traversal run independently, then their scores are f
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [Semantic Extraction](/guides/semantic-extraction): build the graph from raw unstructured text
|
||||
- [Agent Memory](/guides/agent-memory): store, retrieve, and persist agent memories
|
||||
- [Context Graphs](/guides/context-graphs): build and traverse the knowledge graph directly
|
||||
- [Reasoning](/guides/reasoning): derive new facts and run inference rules over the graph
|
||||
- [Decision Intelligence](/guides/decision-intelligence): causal chains, policy enforcement, decision tracking
|
||||
- [LLM Integrations](/guides/llm-integrations): connect Groq, OpenAI, Anthropic, HuggingFace, and 100+ more
|
||||
- [Semantic Extraction](/guides/semantic-extraction) — build the graph from raw unstructured text
|
||||
- [Agent Memory](/guides/agent-memory) — store, retrieve, and persist agent memories
|
||||
- [Context Graphs](/guides/context-graphs) — build and traverse the knowledge graph directly
|
||||
- [Reasoning](reasoning) — derive new facts and run inference rules over the graph
|
||||
- [Decision Intelligence](/guides/decision-intelligence) — causal chains, policy enforcement, decision tracking
|
||||
- [LLM Integrations](/guides/llm-integrations) — connect Groq, OpenAI, Anthropic, HuggingFace, and 100+ more
|
||||
|
||||
+51
-41
@@ -1,87 +1,97 @@
|
||||
---
|
||||
title: "Semantica"
|
||||
description: "The Context and Semantic Layer for AI in High-Stakes Domains: Context Graphs · Decision Intelligence · Full Provenance"
|
||||
description: "The Accountability and Context Layer for AI: Context Graphs · Decision Intelligence · Full Provenance"
|
||||
---
|
||||
|
||||
```bash
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
Most AI agents store embeddings, not meaning. They can't say why a fact was recalled, where it came from, or what led to a decision. In healthcare, finance, legal, and government, that lack of a traceable record blocks production deployment.
|
||||
Your AI agent just made a decision. Now someone needs to explain it.
|
||||
|
||||
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.
|
||||
*What did it know at the time? Which facts shaped the outcome? Where did those facts come from? Has it made the same call before: and did that go well?*
|
||||
|
||||
If your stack can't answer those questions with a traceable record, you have a gap. Not a capability gap: an **accountability gap**. It's the reason AI hasn't landed at scale in healthcare, finance, legal, and government. And it's why teams building for those markets keep rebuilding the same guardrails from scratch.
|
||||
|
||||
**Semantica closes that gap.** It's the context and accountability layer that sits beneath your existing agent framework: not a replacement for LangChain or LlamaIndex, but the infrastructure that makes their outputs trustworthy.
|
||||
|
||||
|
||||
## What Most AI Stacks Are Missing
|
||||
## The Problem Every Production AI Team Hits
|
||||
|
||||
**No memory structure.** Agents store embeddings, not meaning.
|
||||
Powerful agents aren't automatically trustworthy ones. Five structural blind spots make modern AI systems impossible to deploy in regulated environments:
|
||||
|
||||
**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 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 provenance** — outputs can't be traced to source facts
|
||||
- In healthcare, finance, and legal: this is a hard compliance blocker
|
||||
- No lineage from inference back to the original document
|
||||
- No way to demonstrate what the agent actually relied on
|
||||
- Impossible 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 reasoning transparency** — black-box answers with no explanation
|
||||
- Impossible to validate the reasoning path
|
||||
- Impossible to contest a specific conclusion
|
||||
- No basis for improving or correcting future behavior
|
||||
|
||||
**No conflict detection.** Contradictory facts silently coexist in vector stores.
|
||||
**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
|
||||
|
||||
<Note>
|
||||
These aren't edge cases. They're why enterprise AI pilots stall: and why your compliance team keeps saying *not yet*.
|
||||
</Note>
|
||||
|
||||
|
||||
## 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.
|
||||
Semantica gives every agent the infrastructure it needs to be accountable. Drop it into your 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
|
||||
**Context Graphs** — a structured, queryable graph of everything your agent knows, decides, and reasons about
|
||||
- Persistent across agent runs: 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.
|
||||
**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.
|
||||
**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.
|
||||
**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
|
||||
**Temporal Intelligence** — your graph knows not just *what*, but *when*
|
||||
- Allen interval algebra: 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.
|
||||
**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. 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>
|
||||
|
||||
<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' }} />
|
||||
@@ -175,17 +185,17 @@ decision_id = context.record_decision(
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
- [Full Quickstart](/quickstart): step-by-step pipeline walkthrough
|
||||
- [Cookbook](/cookbook): 40+ real-world Jupyter notebooks
|
||||
- [Join Discord](https://discord.gg/sV34vps5hH): community chat and support
|
||||
- [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
|
||||
## Built for Where Mistakes Have Consequences
|
||||
|
||||
Semantica is used in domains where every decision must be explainable and every fact must be traceable.
|
||||
Semantica was designed for 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.
|
||||
**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**
|
||||
@@ -246,7 +256,7 @@ Semantica is used in domains where every decision must be explainable and every
|
||||
- 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
|
||||
- The accountability layer architecture
|
||||
</Step>
|
||||
<Step title="Go deep on any module">
|
||||
Every module has a dedicated [reference page](/reference/context) with:
|
||||
@@ -256,12 +266,12 @@ Semantica is used in domains where every decision must be explainable and every
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
- [Installation](/installation): get Semantica installed in under a minute
|
||||
- [Quickstart](/quickstart): build a complete knowledge graph pipeline in 5 minutes
|
||||
- [Core Concepts](/concepts): the mental model behind the API
|
||||
- [API Reference](/reference/context): exact module, class, and method details
|
||||
- [Cookbook](/cookbook): domain notebooks for real-world use cases
|
||||
- [Changelog](https://github.com/semantica-agi/semantica/releases): release history
|
||||
- [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
|
||||
@@ -384,20 +394,20 @@ Semantica is used in domains where every decision must be explainable and every
|
||||
|
||||
## Why Semantica?
|
||||
|
||||
**Open Source, MIT.** No vendor lock-in, no paywalled features.
|
||||
**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.
|
||||
**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))
|
||||
- Ongoing security hardening: fixes shipped in every release ([CHANGELOG](https://github.com/semantica-agi/semantica/blob/main/CHANGELOG.md))
|
||||
|
||||
**Modular by Design.** Import only what you need.
|
||||
**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
|
||||
- No framework lock-in: works with any agent stack
|
||||
|
||||
@@ -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
@@ -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` |
|
||||
|
||||
|
||||
+21
-21
@@ -30,28 +30,28 @@ icon: "brain"
|
||||
|
||||
## What You Get
|
||||
|
||||
- **AgentContext**: memory, decision tracking, and graph-backed retrieval behind one API
|
||||
- **AgentContext** — Memory, decision tracking, and graph-backed retrieval behind one API
|
||||
- Conversation history and checkpoint diffing
|
||||
- Persist and restore full context state to disk
|
||||
- **ContextGraph**: thread-safe in-memory knowledge graph
|
||||
- **ContextGraph** — Thread-safe in-memory knowledge graph
|
||||
- PageRank, centrality, community detection, temporal validity
|
||||
- Cross-graph navigation and link traversal
|
||||
- **AgentMemory**: embedding-backed memory with retention policy
|
||||
- **AgentMemory** — Embedding-backed memory with retention policy
|
||||
- LRU eviction at configurable `max_memory_size`
|
||||
- Per-conversation history isolation
|
||||
- **DecisionRecorder**: records decisions with causal chains and confidence scores
|
||||
- **DecisionRecorder** — Records decisions with causal chains and confidence scores
|
||||
- Temporal validity windows (`valid_from` / `valid_until`)
|
||||
- Cross-system context capture on every decision
|
||||
- **PolicyEngine**: versioned policy storage in the knowledge graph
|
||||
- **PolicyEngine** — Versioned policy storage in the knowledge graph
|
||||
- Compliance checking against recorded decisions
|
||||
- Policy exception tracking with approver audit trail
|
||||
- **EntityLinker**: maps entity text to stable URIs
|
||||
- **EntityLinker** — Maps entity text to stable URIs
|
||||
- Creates typed links between entity IDs
|
||||
- Prevents "Apple", "Apple Inc.", "AAPL" becoming separate nodes
|
||||
- **ContextRetriever**: fuses vector similarity, graph traversal, and agent memory
|
||||
- **ContextRetriever** — Fuses vector similarity, graph traversal, and agent memory
|
||||
- Richer context than pure vector search
|
||||
- Configurable `hybrid_alpha` and expansion hops
|
||||
- **CausalChainAnalyzer**: traces upstream causes and downstream effects of any decision
|
||||
- **CausalChainAnalyzer** — Traces upstream causes and downstream effects of any decision
|
||||
- Explainability paths with relationship types
|
||||
- Configurable depth and direction
|
||||
|
||||
@@ -273,7 +273,7 @@ icon: "brain"
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Persist your context between runs.** `VectorStore` does not auto-persist; passing `index_path=` to its constructor is a no-op. Call `context.save("agent_state/")` to write memory, the vector index, and the graph to disk, and `context.load("agent_state/")` on the next process to restore them. See the "Persist & Restore" tab under [Real-World Patterns](#real-world-patterns) below.
|
||||
**Persist your context between runs.** `VectorStore` does not auto-persist — passing `index_path=` to its constructor is a no-op. Call `context.save("agent_state/")` to write memory, the vector index, and the graph to disk, and `context.load("agent_state/")` on the next process to restore them. See the "Persist & Restore" tab under [Real-World Patterns](#real-world-patterns) below.
|
||||
</Tip>
|
||||
|
||||
### Memory Methods
|
||||
@@ -449,7 +449,7 @@ print("Nodes: {}, Edges: {}".format(stats["node_count"], stats["edge_count"]))
|
||||
`ContextGraph` exposes a full Distance Intelligence API for exploring semantic neighborhoods and blending proximity into retrieval.
|
||||
|
||||
<Info>
|
||||
Full Distance Intelligence reference (distance matrices, API endpoints, embedding cache, Explorer UI) is covered in the dedicated [Distance Intelligence](/reference/distance) page. This section documents the context-layer API.
|
||||
Full Distance Intelligence reference — distance matrices, API endpoints, embedding cache, Explorer UI — is covered in the dedicated [Distance Intelligence](/reference/distance) page. This section documents the context-layer API.
|
||||
</Info>
|
||||
|
||||
### Neighbors with Distance Metadata
|
||||
@@ -480,7 +480,7 @@ for n in neighbors:
|
||||
| Added field | Type | Description |
|
||||
| :---------- | :---- | :----------- |
|
||||
| `distance_band` | `str` | `"direct"` (1 hop) / `"near"` (2) / `"mid-range"` (3–4) / `"distant"` (5+) |
|
||||
| `confidence_decay` | `float` | `edge_weight ^ hop_count`; decays with each hop |
|
||||
| `confidence_decay` | `float` | `edge_weight ^ hop_count` — decays with each hop |
|
||||
| `path_to_anchor` | `List[str]` | Shortest path from anchor node to this neighbor |
|
||||
| `hop_count` | `int` | BFS depth from anchor |
|
||||
|
||||
@@ -659,7 +659,7 @@ if not receipt.complete:
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Check the receipt. The call returning is not proof the data is gone. FAISS,
|
||||
Check the receipt — the call returning is not proof the data is gone. FAISS,
|
||||
Milvus, and Weaviate expose no delete method, so erasure cannot be completed on
|
||||
those backends today; the receipt reports `unsupported` rather than a success it
|
||||
did not achieve.
|
||||
@@ -687,9 +687,9 @@ At least one store is required; a store that is not supplied reports
|
||||
|
||||
| Status | Meaning |
|
||||
| :--- | :--- |
|
||||
| `erased` | Reached, data removed. On the vectors leg this means the store accepted the delete for the ids given; backends offer no portable existence check, so it is not a count of embeddings that were really there |
|
||||
| `erased` | Reached, data removed. On the vectors leg this means the store accepted the delete for the ids given — backends offer no portable existence check, so it is not a count of embeddings that were really there |
|
||||
| `not_found` | Reached, held nothing for this entity |
|
||||
| `not_configured` | No such store was bound: normal, not a failure |
|
||||
| `not_configured` | No such store was bound — normal, not a failure |
|
||||
| `unsupported` | The store cannot delete at all; retrying will not help |
|
||||
| `failed` | The store was reached and the deletion did not succeed |
|
||||
|
||||
@@ -721,7 +721,7 @@ receipt.to_dict()
|
||||
# }
|
||||
```
|
||||
|
||||
Erasure runs outward-in: vectors, then memory, then the graph. The tombstone is
|
||||
Erasure runs outward-in — vectors, then memory, then the graph. The tombstone is
|
||||
the durable attestation that an erasure happened, so it is written last: a crash
|
||||
mid-cascade leaves the node present and the receipt incomplete, rather than a
|
||||
tombstone claiming more than actually happened. A store that raises is recorded
|
||||
@@ -1087,10 +1087,10 @@ class EntityLink:
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
- [Vector Store](/reference/vector_store): embedding storage backend for memory retrieval.
|
||||
- [Knowledge Graph](/reference/kg): graph algorithms and analytics used inside ContextGraph.
|
||||
- [Reasoning](/guides/reasoning): logical inference layered on top of context.
|
||||
- [Provenance](/guides/provenance): W3C PROV-O lineage for every stored fact.
|
||||
- [Vector Store](/reference/vector_store) — Embedding storage backend for memory retrieval.
|
||||
- [Knowledge Graph](/reference/kg) — Graph algorithms and analytics used inside ContextGraph.
|
||||
- [Reasoning](reasoning) — Logical inference layered on top of context.
|
||||
- [Provenance](provenance) — W3C PROV-O lineage for every stored fact.
|
||||
|
||||
- [Context Module](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/19_Context_Module.ipynb): memory and decision tracking · Intermediate
|
||||
- [Advanced Context Engineering](https://github.com/semantica-agi/semantica/blob/main/cookbook/advanced/11_Advanced_Context_Engineering.ipynb): production FAISS + Neo4j setup · Advanced
|
||||
- [Context Module](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/19_Context_Module.ipynb) — Memory and decision tracking · Intermediate
|
||||
- [Advanced Context Engineering](https://github.com/semantica-agi/semantica/blob/main/cookbook/advanced/11_Advanced_Context_Engineering.ipynb) — Production FAISS + Neo4j setup · Advanced
|
||||
|
||||
@@ -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
@@ -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");
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -416,38 +416,6 @@ class WeaviateStore:
|
||||
)
|
||||
raise ProcessingError(f"Failed to add objects: {str(e)}")
|
||||
|
||||
def delete_vectors(self, vector_ids: List[str], **options) -> Dict[str, Any]:
|
||||
"""Delete vectors (objects) from the collection by their ids.
|
||||
|
||||
Args:
|
||||
vector_ids: Object uuids to delete
|
||||
**options: Additional options (ignored, kept for API parity)
|
||||
|
||||
Returns:
|
||||
A dict with the number of successfully deleted objects
|
||||
(``delete_count``).
|
||||
"""
|
||||
if self.collection is None or not WEAVIATE_AVAILABLE:
|
||||
raise ProcessingError("Collection not initialized or Weaviate unavailable")
|
||||
|
||||
if not vector_ids:
|
||||
return {"delete_count": 0}
|
||||
|
||||
deleted = 0
|
||||
try:
|
||||
data = self.collection.data
|
||||
for vector_id in vector_ids:
|
||||
if not vector_id:
|
||||
continue
|
||||
# delete_by_id returns False (not an error) for a uuid that is
|
||||
# not present, and True when an object was deleted. Count only
|
||||
# actual deletes so delete_count never over-reports.
|
||||
if data.delete_by_id(vector_id):
|
||||
deleted += 1
|
||||
return {"delete_count": deleted}
|
||||
except Exception as e:
|
||||
raise ProcessingError(f"Failed to delete vectors: {str(e)}")
|
||||
|
||||
def get_vector(self, vector_id: str) -> Optional[np.ndarray]:
|
||||
"""Get vector by ID."""
|
||||
if self.collection is None or not WEAVIATE_AVAILABLE:
|
||||
|
||||
@@ -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"
|
||||
@@ -1,142 +0,0 @@
|
||||
"""Tests for WeaviateStore.delete_vectors (#1374)."""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from semantica.context.erasure import STATUS_ERASED, ErasureCoordinator
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
from semantica.vector_store import VectorStore
|
||||
from semantica.vector_store.weaviate_store import WeaviateStore
|
||||
|
||||
|
||||
class WeaviateStoreDeleteVectorsTest(TestCase):
|
||||
def setUp(self):
|
||||
self.patches = [
|
||||
patch("semantica.vector_store.weaviate_store.WEAVIATE_AVAILABLE", True)
|
||||
]
|
||||
for p in self.patches:
|
||||
p.start()
|
||||
|
||||
def tearDown(self):
|
||||
for p in reversed(self.patches):
|
||||
p.stop()
|
||||
|
||||
def _store(self, error=None):
|
||||
"""Return (store, data) where data records delete_by_id calls."""
|
||||
data = MagicMock()
|
||||
data.delete_by_id = MagicMock()
|
||||
coll = MagicMock()
|
||||
coll.data = data
|
||||
if error is not None:
|
||||
data.delete_by_id.side_effect = error
|
||||
store = WeaviateStore()
|
||||
store.collection = coll
|
||||
return store, data
|
||||
|
||||
def test_delete_single_id_calls_delete_by_id(self):
|
||||
store, data = self._store()
|
||||
ret = store.delete_vectors(["abc"])
|
||||
data.delete_by_id.assert_called_once_with("abc")
|
||||
self.assertEqual(ret, {"delete_count": 1})
|
||||
|
||||
def test_delete_many_ids_calls_each(self):
|
||||
store, data = self._store()
|
||||
ret = store.delete_vectors(["a", "b", "c"])
|
||||
self.assertEqual(data.delete_by_id.call_count, 3)
|
||||
self.assertEqual(ret, {"delete_count": 3})
|
||||
|
||||
def test_delete_skips_ids_that_report_missing(self):
|
||||
store, data = self._store()
|
||||
|
||||
def _fake(uuid):
|
||||
return uuid != "missing"
|
||||
|
||||
data.delete_by_id.side_effect = _fake
|
||||
ret = store.delete_vectors(["present", "missing", "also-here"])
|
||||
self.assertEqual(data.delete_by_id.call_count, 3)
|
||||
self.assertEqual(ret, {"delete_count": 2})
|
||||
|
||||
def test_delete_drops_empty_ids(self):
|
||||
store, data = self._store()
|
||||
store.delete_vectors(["", "abc"])
|
||||
data.delete_by_id.assert_called_once_with("abc")
|
||||
self.assertEqual(data.delete_by_id.call_count, 1)
|
||||
|
||||
def test_delete_empty_ids_is_noop(self):
|
||||
store, data = self._store()
|
||||
ret = store.delete_vectors([])
|
||||
self.assertEqual(ret, {"delete_count": 0})
|
||||
data.delete_by_id.assert_not_called()
|
||||
|
||||
def test_delete_without_collection_raises(self):
|
||||
store = WeaviateStore()
|
||||
with self.assertRaises(ProcessingError):
|
||||
store.delete_vectors(["a"])
|
||||
|
||||
def test_delete_backend_error_raises_processing_error(self):
|
||||
store, _ = self._store(error=RuntimeError("connection reset"))
|
||||
with self.assertRaises(ProcessingError):
|
||||
store.delete_vectors(["a"])
|
||||
|
||||
|
||||
class WeaviateErasureIntegrationTest(TestCase):
|
||||
"""ErasureCoordinator reaches the real WeaviateStore.delete_vectors path."""
|
||||
|
||||
def setUp(self):
|
||||
self._patch = patch(
|
||||
"semantica.vector_store.weaviate_store.WEAVIATE_AVAILABLE", True
|
||||
)
|
||||
self._patch.start()
|
||||
|
||||
def tearDown(self):
|
||||
self._patch.stop()
|
||||
|
||||
def _bind_weaviate_as_vector_store(self):
|
||||
vs = VectorStore(backend="weaviate", config={"dimension": 3})
|
||||
weaviate = WeaviateStore()
|
||||
data = MagicMock()
|
||||
coll = MagicMock()
|
||||
coll.data = data
|
||||
weaviate.collection = coll
|
||||
vs._backend_store = weaviate
|
||||
return vs, data
|
||||
|
||||
def test_erasure_reports_erased_when_delete_runs(self):
|
||||
vs, data = self._bind_weaviate_as_vector_store()
|
||||
coord = ErasureCoordinator(vector_store=vs)
|
||||
receipt = coord.erase_entity("customer-4471")
|
||||
data.delete_by_id.assert_called()
|
||||
self.assertEqual(receipt.stores["vectors"]["status"], STATUS_ERASED)
|
||||
|
||||
def test_erasure_reports_erased_when_nothing_was_found(self):
|
||||
"""delete_by_id returns False (404) for an id that is not in the store.
|
||||
|
||||
For erasure that still means the goal is met: nothing remains under
|
||||
that id. The receipt keeps the honest zero count in backend_result
|
||||
instead of raising a false failed status.
|
||||
"""
|
||||
vs, data = self._bind_weaviate_as_vector_store()
|
||||
data.delete_by_id.return_value = False
|
||||
coord = ErasureCoordinator(vector_store=vs)
|
||||
receipt = coord.erase_entity("customer-4471")
|
||||
self.assertEqual(receipt.stores["vectors"]["status"], STATUS_ERASED)
|
||||
self.assertEqual(
|
||||
receipt.stores["vectors"]["backend_result"], {"delete_count": 0}
|
||||
)
|
||||
|
||||
def test_erasure_backend_name_is_weaviate(self):
|
||||
vs, _ = self._bind_weaviate_as_vector_store()
|
||||
coord = ErasureCoordinator(vector_store=vs)
|
||||
receipt = coord.erase_entity("customer-4471")
|
||||
self.assertEqual(receipt.stores["vectors"]["backend"], "weaviate")
|
||||
|
||||
def test_facade_delete_vectors_forwards_to_weaviate(self):
|
||||
vs, data = self._bind_weaviate_as_vector_store()
|
||||
|
||||
def _fake(uuid):
|
||||
return uuid != "missing"
|
||||
|
||||
data.delete_by_id.side_effect = _fake
|
||||
ret = vs.delete_vectors(["present", "missing"])
|
||||
self.assertEqual(data.delete_by_id.call_count, 2)
|
||||
self.assertEqual(ret, {"delete_count": 1})
|
||||
Reference in New Issue
Block a user