Compare commits

..
Author SHA1 Message Date
Zohaib Hassnain d0d6b9ab5c Merge remote-tracking branch 'origin/main' into docs-evals-reference-actual-api
# Conflicts:
#	docs/reference/evals.md
2026-09-03 14:12:14 +05:00
b177fa7556 fix: replace mutable default arguments with None + in-body defaults (#1068)
* fix mutable default argument in graph_analyzer.py

* fix mutable default argument in kg_chunkers.py

* fix mutable default argument in methods.py

* Address review: move default-init code out of docstrings, default levels in split_hierarchical

Three findings from the Qodo review:

- analyze_temporal_evolution: the 'if metrics is None' block had landed
  inside the docstring, so it never executed and metrics_tracked came back
  None. Moved below the docstring where it runs.

- HierarchicalChunker.__init__: the same misplacement turned the docstring
  into a dead string constant and broke help()/introspection. Moved the
  default-init below it.

- split_hierarchical: the signature now defaults levels to None, but the
  body still ran 'in levels' membership tests — calling it without levels
  raised TypeError. Defaults to the documented hierarchy, matching the
  class-level default.

* test: add mutable-default regression tests for the three fixed sites

- tests/split/test_chunkers.py: TestMutableDefaultRegression (6 tests)
  - split_hierarchical() default levels and chunk_sizes stay independent across calls
  - HierarchicalChunker() default levels stay independent across instances

- tests/kg/test_kg.py: TestAnalyzeTemporalEvolutionMutableDefault (5 tests)
  - analyze_temporal_evolution() default metrics value is canonical
  - mutations to a returned metrics_tracked list do not affect the next call
  - explicit metrics override is forwarded and reflected in the return value
  - mutating an explicitly passed list does not corrupt a subsequent default call

All 96 tests in the two affected test files pass.

---------

Co-authored-by: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com>
Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
2026-09-03 13:18:03 +05:30
Mohd Kaif 4559ac6536 Merge pull request #1407 from Duansg/fix-1405
docs: convert relative page links to root paths to fix 404s on the live site
2026-09-03 13:02:30 +05:30
Sameer Kadam b17ce71f56 Merge branch 'main' into docs-evals-reference-actual-api 2026-09-03 13:00:39 +05:30
Mohd Kaif 39c35549d2 Merge branch 'main' into fix-1405 2026-09-03 12:53:38 +05:30
Mohd Kaif d54d74c810 Merge pull request #1410 from semantica-agi/fix-required-ci-checks-docs
ci: report required checks for docs-only PRs
2026-09-03 12:41:08 +05:30
Sameer6305 2086a21615 fix(ci): fail-closed detector, merge-base diff, any-depth markdown filter 2026-09-03 12:13:51 +05:30
Sameer6305 b4af22d724 ci: report required checks for docs-only PRs 2026-09-03 12:03:51 +05:30
Sameer6305 6a2173027e docs(evals): fix evaluator behavior details 2026-09-03 11:05:50 +05:30
Sameer Kadam 66632a9437 Merge branch 'main' into docs-evals-reference-actual-api 2026-09-03 10:40:48 +05:30
Duansg a59688c6f9 additional fixes 2026-09-02 20:37:40 -07:00
Duansg 40466269b8 docs: convert relative page links to root paths to fix 404s on the live site 2026-09-02 20:20:55 -07:00
Zohaib Hassnain 38ae5b580b docs: fix two broken cookbook notebook links (#1403)
* docs: fix two dead notebook links

* docs(learning-more): describe the embeddings notebooks
2026-09-03 04:21:21 +05:00
Zohaib Hassnain 279fdbf15b docs(quickstart): qodo findings addressed (#1402) 2026-09-03 04:18:17 +05:00
Harsh Arora 45915e50a3 fix(context): vector_store=False must suppress AgentMemory's internal vector cascade in ErasureCoordinator (#1395)
* fix(erasure): ensure vector_store=False disables internal vector cascade in AgentMemory

* fix(erasure): ensure skip_vector=True does not orphan local vector ID tracking
2026-09-03 04:05:53 +05:00
Zohaib Hassnain b7b60d4a17 docs(quickstart): fix broken code against real APIs (#1401) 2026-09-03 04:05:19 +05:00
Zohaib Hassnain 25d2ea5fe9 docs: update stale latest version claims 2026-09-03 03:50:50 +05:00
Zohaib Hassnain 3c68cd12ad docs(mcp): correct tool count (#1399) 2026-09-03 03:40:52 +05:00
Sameer Kadam 798a7455e4 fix(mcp): complete persistence and setup fixes (#1394)
MCP's stdio transport uses stdout for JSON-RPC framing, so anything else written there corrupts every response after it. The original #1134 bug was progress-tracker output landing on stdout during tool calls that construct a `ContextGraph`, which is exactly what happens on any request that triggers reasoning or extraction. This PR closes out the remaining pieces of that fix: loading now goes through `load_from_file()` instead of the older `load()` path on the root graph, and mutations, `record_decision`, `add_entity`, `add_relationship`, now persist back to `SEMANTICA_KG_PATH` when it's configured, in both MCP server implementations (the root `mcp/` package and the packaged `semantica.mcp_server`), not just one.

Four things came out of review on top of that.

The stdio regression test originally exercised `get_graph_summary`, which doesn't touch the progress tracker at all, so it couldn't have caught the original bug. Swapped it for `run_reasoning`: `Reasoner.infer_with_results()` calls `progress_tracker.start_tracking()` directly, the exact call site that corrupted stdout before, so this is the minimal path that actually proves the fix. The test now spawns a real `python -m mcp` subprocess, sends it a `tools/call` for `run_reasoning`, and asserts every single line on stdout parses as JSON.

Loading a corrupt or unreadable `SEMANTICA_KG_PATH` used to fail silently and fall through to an empty graph, which meant the next mutation would happily save that empty graph over the original file. Both implementations now track whether the initial load actually succeeded. If it didn't, every mutation handler refuses to save and returns an error instead, so a broken file on disk stays broken rather than getting silently replaced with nothing. An empty file is treated differently: that's a fresh destination, not a corrupt one, and starts a normal empty graph without tripping the guard.

`save_to_file` used to `open(path, 'w')` and `json.dump` directly into the destination, so a crash or disk-full error mid-write could leave a truncated file as the only copy of the graph. It now writes to a temp file in the same directory, flushes, fsyncs, and only then `os.replace`s the destination, so the destination is always either the old contents or the new contents, never a partial write. The temp file gets cleaned up if anything fails before the replace.

And since a mutation is applied to the in-memory graph before the save happens, a save failure used to leave the in-memory graph ahead of what's on disk, an entity or decision the client thinks succeeded but that never made it to the file. `record_decision`, `add_entity`, and `add_relationship` all roll back the in-memory mutation now if `save_to_file` raises, so the client-visible state and the persisted state never diverge: either both hold the change or neither does.

104 tests passing across the MCP, persistence, and progress-tracking suites.
2026-09-03 03:35:20 +05:00
Zohaib Hassnain f83d2a8b12 document evals API 2026-09-03 03:19:09 +05:00
Mohd Kaif bd584b7402 Update features list in README
Removed 'Self-Hostable' and 'Auditable' from the features list.
2026-09-02 22:21:41 +05:30
Mohd Kaif a4500f5b20 Merge pull request #1396 from semantica-agi/readme-enterprise-connectors-update
docs: tighten README audience list, add SAP connector mentions, log Salesforce ingestor
2026-09-02 21:50:51 +05:30
KaifAhmad1 bdd12e8ac6 fix: correct JWT auth requirements in changelog, add missing SAP install extra
- CHANGELOG: JWT Bearer requires username too, not just consumer_key + private key
- README: add pip install semantica[ingest-sap] to the install-extras list, which was missing despite SAP appearing in the supported-sources lists

Addresses Qodo review feedback on #1396.
2026-09-02 21:45:24 +05:30
KaifAhmad1 48204d4e02 docs: tighten README audience list, add SAP to connector mentions, log unreleased Salesforce ingestor
- Trim "Who it's for" bullets in README for concision
- Propagate SAP OData connector mentions across README's integration lists (was only in the What's New section)
- Add missing CHANGELOG entry for the unreleased Salesforce ingestor (#1240)
- Remove sample `semantica doctor` output lines from the quickstart snippet
2026-09-02 21:23:29 +05:30
Zohaib Hassnain 1bc873cbbd Merge pull request #1328 from semantica-agi/feat/pinecone-iter-all
feat(vector_store): add pinecone iter_all
2026-09-02 20:07:54 +05:30
KaifAhmad1 4dd88375e1 fix(vector_store): don't short-circuit pinecone iter_all() on an empty page
if not vector_ids: return fired before the continuation token was ever
checked. Pinecone's actual pagination contract is that a scan is only
exhausted when the response carries no pagination token -- a page can
legitimately list zero ids while pagination.next is still set (sparse
or filtered namespaces, eventual-consistency windows on serverless
indexes). This was flagged in review but the fix commit that followed
only addressed the separate repeated-token stall case, not this one.

Reproduced concretely against the unfixed code: a page with data,
followed by an empty page with a live token, followed by a page with
more data -- the last page was silently dropped with no error raised,
exactly the #1083 failure mode (store migrate reporting success after
copying only part of a collection).

Now the empty-page case skips the pointless fetch() call but still
falls through to the same next_token check every other path already
goes through, so a live token continues the scan and only a genuinely
absent token (or one that's stopped advancing) ends it.

Added test_continues_past_an_empty_page_with_a_live_token, the
"empty page + non-None next token" case the original review asked for
and that wasn't otherwise covered.
2026-09-02 19:54:24 +05:30
Mohd Kaif fc899c6966 Merge pull request #1326 from semantica-agi/feat/milvus-iter-all
feat(vector_store): add milvus iter_all
2026-09-02 19:37:54 +05:30
KaifAhmad1 98bd632585 Merge remote-tracking branch 'origin/main' into feat/milvus-iter-all 2026-09-02 19:20:55 +05:30
KevinandSameer Kadam 30a91a3a78 feat(evals): add per-metric objective support to runner (closes #1091) (#1092)
* chore: ignore .worktrees directory

* feat(evals): add eval metric and result models

* feat(evals): add evaluator registry

* feat(evals): add exact/regex/range/length evaluators

* feat(evals): add keyword/levenshtein/rouge/llm-as-judge evaluators

* feat(evals): add decision_scores composite evaluator

* feat(evals): add evaluation runner

* feat(evals): expose public API and module proxy

* fix(evals): resolve __all__ names and repair usage example

* docs(evals): add usage docs and changelog entry

* style(evals): tidy evaluator metadata and wiring comments

* fix(evals): honor expected arg and classify error metrics

* fix(evals): export get_evaluator and fix shared meta default

* fix(evals): guard provenance check against non-dict metadata

* docs: add objective layer design spec for semantica.evals

* docs: refine objective spec for consistency with AIP Evals semantics

* docs: add implementation plan for evals objective layer

* docs: fix plan tests to use module-level pytest import

* feat(evals): add per-metric objective support to runner

* docs(evals): document per-metric objectives

* docs(evals): fix minimize example threshold to demonstrate pass

* fix(evals): validate objective config shape strictly

* docs(evals): clarify objective examples and Boolean semantics

* fix(evals): honor direction-only minimize, fail fast on objectives, deep-merge case config

- minimize without threshold is now a no-op, matching maximize (issue #1091
  requires thresholds to be optional for both directions)
- objective config is parsed for every case before any target_fn/evaluator
  runs, so an invalid per-case objective rejects the run up front
- per-case evaluator config deep-merges over the global config so a case
  that overrides one setting keeps the run-level objective
- regression tests for all three, plus updated docs/CHANGELOG

Addresses 3 of 4 Qodo findings on #1092 (the 4th, 'result models defined
twice', is a false positive: types live in types.py)

* fix: finalize eval objectives review

---------

Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
2026-09-02 18:47:28 +05:30
Mohd Kaif 23126106a3 Merge pull request #1317 from semantica-agi/feat/weaviate-iter-all
feat(vector_store): add weaviate iter_all
2026-09-02 18:39:57 +05:30
Mohd Kaif 4b001b4c9d Merge branch 'main' into feat/weaviate-iter-all 2026-09-02 18:29:14 +05:30
KaifAhmad1 6c9eb2296d fix(vector_store): don't treat an empty weaviate page as end of scan
iter_all() unconditionally returned on any empty fetch_objects() page,
regardless of pagination mode. That's safe for offset/single_page (an
empty page there is a direct, unambiguous statement about live rows),
but not for cursor mode: `after` has no server-issued continuation
value of its own, it's derived client-side from the last object's uuid,
so an empty page gives nothing to advance it with. If Weaviate's cursor
walks internal storage position rather than strict uuid order, a batch
can in principle land entirely on a gap (e.g. tombstoned objects) with
live data past it -- the same risk already confirmed and fixed for
Qdrant's scroll cursor in #1316. Reproduced concretely against the
pre-fix code: a full page followed by an empty page followed by a page
with real data silently dropped that last page with no error raised.

iter_all() now falls back to offset pagination once when a cursor-mode
page comes back empty, rather than assuming that's the end. Offset
addresses live rows directly by position and has no equivalent gap, so
an empty page there (or in single_page mode) is trustworthy and still
ends the scan immediately.

Also updates test_iter_all_empty_collection_yields_nothing and
test_iter_all_requests_vectors, which needed a second empty page now
that a genuinely empty collection takes two calls (cursor, then the
confirming offset check) to report as such.
2026-09-02 17:43:52 +05:30
KaifAhmad1 1ad17beaf6 fix(vector_store): sync qdrant iter_all() with #1316's stall-guard fix
This branch was forked from an earlier commit of feat/vector-store-iter-all
(#1316), before that PR fixed a false-positive/silent-truncation bug in
QdrantStore.iter_all(): an empty scroll page with a still-advancing cursor
(e.g. a window landing entirely on tombstoned points) was treated as the
end of the collection instead of continuing. Syncing qdrant_store.py,
vector_store.py, and their tests to #1316's current tip (fa967983) so this
branch doesn't reintroduce the already-fixed bug once merged. Content-only
sync of the 4 shared files (verified via diff against origin/feat/vector-store-iter-all)
rather than a full branch merge, to avoid pulling in unrelated main drift
that has landed on that branch since this one diverged.
2026-09-02 17:39:42 +05:30
Zohaib Hassnain 110f6deb1e Merge pull request #1316 from semantica-agi/feat/vector-store-iter-all
feat(vector_store): add iter_all enumeration for cursor-based backends
2026-09-02 17:37:11 +05:30
Zohaib Hassnain b8299b1427 chore: clean it 2026-09-02 17:19:19 +05:30
Zohaib Hassnain fa967983e6 fix(vector_store): dedupe qdrant record conversion, don't abort iter_all on a live cursor with an empty page 2026-09-02 17:19:19 +05:30
Zohaib Hassnain bbd423c50a fix(vector_store): raise on stalled pinecone pagination instead of truncating 2026-09-02 17:19:19 +05:30
Zohaib Hassnain fcdad56893 making it clean 2026-09-02 17:19:19 +05:30
Zohaib Hassnain 6b36379f15 feat(vector_store): add pinecone iter_all 2026-09-02 17:19:19 +05:30
Zohaib Hassnain f2e7b9ed75 fix(vector_store): raise instead of truncating when a qdrant scan cannot advance 2026-09-02 17:19:19 +05:30
Zohaib Hassnain 5e80ebd837 fix(vector_store): drop qdrant migrate wiring, keep iter_all only
VectorStore cannot actually migrate to or from qdrant yet. _init_backend_store constructs QdrantStore without connecting or selecting a collection, so reads raise a Collection not initialized error, and the facade store_vectors dispatches only to add/add_vectors while QdrantStore exposes insert_vectors, so writes raise NotImplementedError.

Both are pre-existing facade gaps that nothing had exposed, since migrate previously only allowed faiss/sqlite/pgvector. Adding qdrant to the allowlist claimed support that does not work end to end, so it is removed along with the dimension inference that only fires for backends missing a .dimension attribute. Tracked separately; this PR keeps just the iter_all primitive.
2026-09-02 17:19:19 +05:30
Zohaib Hassnain d175f894a4 feat(vector_store): add iter_all enumeration and wire up qdrant migration 2026-09-02 17:19:19 +05:30
Mohd Kaif 3d32254b07 Merge pull request #1390 from semantica-agi/fix/security-scan-pip-audit-migration
fix(ci): migrate security-scan from Safety to pip-audit
2026-09-02 16:39:01 +05:30
KaifAhmad1 b5199ae6e3 fix(ci): handle pip-audit skipped dependencies, restore manual trigger, fix stale docs
Addresses review feedback on this PR:

- Guard 2 and the PR-comment JS parser both required every dependency
  in pip-audit's report to carry an array-valued `vulns` field. A
  dependency pip-audit can't resolve/audit is reported instead as
  {"name": ..., "skip_reason": ...} with no `vulns` key at all (see
  pip_audit._format.json.JsonFormat._format_dep) - a normal, documented
  shape, not a malformed one. That made a single unauditable package
  hard-fail the whole job and show "Invalid report structure" in the PR
  comment, reintroducing the same class of scan-unrelated CI break this
  migration was meant to fix for Safety. Both now accept skipped
  entries, treat them as zero vulns, and surface them explicitly (job
  log + PR comment) instead of silently dropping or crashing on them.
  Verified the fixed jq queries and JS parse logic against synthetic
  pip-audit report fixtures covering the normal, skipped, and malformed
  shapes.

- Restored a `workflow_dispatch` trigger on security-scan.yml. Deleting
  security.yml (which had it) left no way to manually run a dependency
  audit on demand.

- Updated SECURITY.md, which still described security.yml as a live
  scanning workflow and Safety as an active scanner after this PR
  deletes both.
2026-09-02 16:22:16 +05:30
Mohd Kaif db48f73755 Merge branch 'main' into fix/security-scan-pip-audit-migration 2026-09-02 16:01:17 +05:30
Zohaib Hassnain 07113d2d2d fix(ci): migrate security scan from Safety to pip audit 2026-09-02 15:21:36 +05:00
Shubham SrivastavaandSameer Kadam 909ccf0ded test: install extractor dispatch mocks per test, not at module scope (#1337)
The module assigned MagicMocks into sys.modules at import time and never
removed them. pytest imports every test module during collection before
running anything, so those mocks were live while later modules were
imported and each bound them into its own globals.

132 tests passed alone and failed in a full-suite run as a result. Full
suite goes from 199 failed / 5506 passed to 67 failed / 5638 passed.

A tearDownModule cannot fix this: collection has already finished by the
time it runs. The extractors resolve 'from .methods import
get_entity_method' lazily inside their methods, so the stand-in only has
to be in sys.modules while a test executes - it is now installed per test
via patch.dict in setUp and removed by addCleanup.

Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
2026-09-02 15:31:38 +05:30
Guofang.Tang fb69b033be fix(ontology): resolve endpoints in direct property inference (#1229)
The relationship-endpoint fix merged in #1170 covers the main ontology generation pipeline, but the public property-inference path still had the same gap.

`OntologyGenerator.infer_properties()`, and the `PropertyGenerator` it delegates to, fell back to `owl:Thing` for both domain and range when a relationship used entity IDs or aliases instead of explicit `source_type` / `target_type` values. The pipeline resolved those endpoints correctly, but the public API path did not.

This moves the existing alias-building and endpoint-resolution logic out of `OntologyGenerator` and into a shared `relationship_utils` module:

* `build_entity_aliases`
* `get_relationship_endpoint`
* `resolve_relationship_endpoint_type`

`OntologyGenerator` now uses those shared helpers instead of keeping its own copies.

`PropertyGenerator._infer_object_properties()` now also receives the entity list, builds the same alias index, and uses the shared endpoint resolver. This replaces the old fallback:

```python
rel.get("source_type") or self._infer_class_from_entity(...)
```

which could only fall back to `owl:Thing` because `_infer_class_from_entity()` never actually resolved an entity.

There are two small behavior changes from centralizing the logic. `build_entity_aliases()` now converts `entity_type` to `str` before adding it to the alias set, avoiding mixed-type alias values. `resolve_relationship_endpoint_type()` returns `None` rather than `""` when there is no usable explicit type, since an empty string isn't a meaningful endpoint type.

The new `test_public_infer_properties_resolves_id_endpoints` covers the broken public API path directly. It creates entities and ID-based relationships through `infer_classes()` / `infer_properties()` and verifies that the inferred `worksFor` property resolves to `Person` for the domain and `Organization` for the range instead of falling back to `owl:Thing`.

That exercises the same endpoint-resolution behavior already covered by the pipeline tests, but through the public entry point that was still missing it.
2026-09-02 14:26:13 +05:00
Guofang.Tang c10090dc9b fix(ci): fail closed on malformed Safety reports (#1366)
The Security Scan workflow already scans `requirements-ci.txt` directly, but malformed Safety output could still be treated as a clean scan. If the report existed on disk but `vulnerabilities` was missing, `null`, or the wrong type, the workflow could end up counting it as zero findings.

This adds a structural check immediately after the report is written. `vulnerabilities` must be an array; otherwise the step fails closed with a clear error instead of treating a broken report as a successful scan.

There was a related problem in the PR reporting path. The comment step already knew how to render an `Invalid report structure` warning, but that branch was effectively unreachable. In GitHub Actions, a custom `if:` is implicitly gated by `success()` unless it includes a status function such as `always()` or `failure()`. Once the Safety step exited non-zero, the Upload and Comment steps were skipped, so the warning could never be posted.

Fixing that required changing how Safety failures flow through the job rather than just adding another guard. The Safety step now uses `continue-on-error: true`, which lets Bandit and Semgrep continue running and allows the Upload and Comment steps to process the failed or malformed Safety result.

Because `continue-on-error` means the Safety step no longer carries the job's final failure signal itself, the workflow now tracks that state explicitly with `SAFETY_SCAN_STATUS`. It is set to `failed` at the start of the Safety step, before any validation runs, and changes to `passed` only when the report is valid and contains zero vulnerabilities.

That default-failed behavior covers every other exit path: a missing report, malformed `vulnerabilities` field, invalid vulnerability count, Safety failure, or an actual vulnerability finding all leave the status as `failed`.

A final `Enforce Safety Gate` step checks `SAFETY_SCAN_STATUS` and fails the job unless it is exactly `passed`. This keeps the same merge-blocking behavior while still allowing the rest of the security checks and reporting steps to run after a Safety failure.

This is a follow-up to #1356. The overlapping Safety behavior changes and duplicate pip-audit path from that PR were dropped after `main` picked up the canonical fix for the underlying `cuda-toolkit` crash. This change keeps only the report-validation hardening that remains independent of that fix.
2026-09-02 14:12:33 +05:00
Zohaib Hassnain 28c96c2539 fix(ci): update actions/deploy-pages pin to current v5 (v5.0.1) (#1387) 2026-09-02 13:59:42 +05:00
Ahmad Bilal 170b4215a6 fix(vector_store): persist vector_ids and metadata across FAISS index save/load (#1272) (#1314)
`FAISSIndex.save()` previously wrote only the raw FAISS index. `load()` then rebuilt the wrapper with empty `vector_ids` and `metadata`, so that state was never restored.

That made a save/load round trip effectively unusable through the wrapper API: `scan_vectors()` returned no vectors, `count()` returned `0`, and `get_vector(id)` returned `None` for IDs that were present in the underlying FAISS index.

This also affected migration. `semantica store migrate --from faiss` could load a valid FAISS index, see zero vectors through `scan_vectors()`, migrate nothing, and still exit successfully. Since FAISS is a supported migration source, this was a silent data-loss path rather than just a persistence bug.

The fix adds a `.meta.json` sidecar next to the FAISS binary. It stores:

* `vector_ids`
* `metadata`
* `dimension`
* `index_type`

The sidecar is written atomically using a temporary file and rename. `save()` also serializes the metadata before writing the FAISS binary, so a serialization error fails before either persistence artifact is created. That avoids leaving a valid-looking index file behind without the metadata needed to use it correctly.

On load, `dimension` and `index_type` come from the sidecar rather than the caller's arguments. This makes the reconstructed wrapper reflect the index that was actually saved instead of relying on the caller to provide matching values.

There are also explicit checks for incomplete or inconsistent persisted state. If the sidecar is missing, which can happen with indexes written by older versions or when only the FAISS binary was copied, `load()` emits a `RuntimeWarning` instead of silently returning an apparently usable wrapper with no IDs or metadata. If the number of saved vector IDs doesn't match the FAISS index's `ntotal`, `load()` raises `ProcessingError` rather than returning a state where vectors exist in FAISS but can't be reached through `scan_vectors()`.

Metadata serialization changed during review as well. The first version used `json.dumps(..., default=str)`. That avoided failures for values such as `datetime`, `UUID`, and `set`, but it was lossy: those values came back as strings instead of their original Python types.

That was replaced with a tagged encoder/decoder that preserves the supported types across a round trip. It currently handles sets, datetimes, dates, UUIDs, NumPy scalars and arrays, and bytes, with bytes stored as base64.

The decoder also uses an exact-schema check for tagged values. A normal dictionary that happens to contain a reserved tag key alongside other fields is left alone instead of being interpreted as an encoded type.

The final implementation was spread across fourteen commits, mostly following review feedback. Those changes included cleaning up conflict markers from an unfinished stash pop, expanding round-trip and retry coverage, adding the missing-sidecar warning, adding an end-to-end `scan_vectors()` persistence test, replacing lossy metadata serialization with the tagged format, checking FAISS/sidecar count mismatches, adding `bytes` support, and reordering `save()` so metadata serialization happens before the FAISS index is written.
2026-09-02 13:44:42 +05:00
Zohaib Hassnain 5f600a3f36 fix(ci): ignore SFTY-20260723-60537 (CVE-2026-65918) in torchvision, unreachable transitive dep (#1385) 2026-09-02 13:37:03 +05:00
Mohd Kaif 1d18755a4e Delete cookbook/advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb 2026-09-02 13:54:13 +05:30
Mohd Kaif 3acf801273 Merge pull request #1361 from taoche/fix/semantic-layer-basics-intro
docs(cookbook): rewrite Semantic Layer Basics as an introductory workflow
2026-09-02 13:22:42 +05:30
KaifAhmad1 796f181c75 docs(cookbook): fix stale RDFExporter claim, pin oxigraph install version
Step 5 explicitly avoids RDFExporter's compact projection and builds
the Turtle export from the TripletStore's own triples instead, but the
Summary cell still credited RDFExporter -- a leftover from before the
rdflib-based export replaced it. Correct the claim to match the code.

Also pin the install to >=0.6.7: earlier releases could return
ontology classes with an empty uri (#1103), which made entity_type
mappings silently resolve to None instead of raising, so the notebook
would appear to pass while never actually typing its instances.
2026-09-02 13:15:14 +05:30
Mohd Kaif 8e73ed8d4c Merge branch 'main' into fix/semantic-layer-basics-intro 2026-09-02 12:44:19 +05:30
Mohd Kaif 114641b39d Merge pull request #1359 from taoche/fix/cookbook-08-end-to-end
docs(cookbook): make notebook 08 a real rerunnable KG workflow
2026-09-02 12:26:00 +05:30
Mohd Kaif f71b711205 Merge branch 'main' into fix/cookbook-08-end-to-end 2026-09-02 12:11:43 +05:30
Mohd Kaif f9a661a4ed security(deps-dev): bump browserslist from 4.28.2 to 4.28.8 in /explorer (#1382)
Fixes GHSA-73wf-gq98-2v4g (prototype pollution / DoS via unguarded
browserslist-stats.json parsing) and GHSA-c83g-rgw3-j3cx (unbounded
cache growth leading to OOM), both patched upstream in 4.28.7.
2026-09-02 11:49:55 +05:30
Zohaib Hassnain af829f5f20 fix(vector_store): don't let iterator close() mask the real scan error, dedupe milvus result shaping, split unavailable/uninitialized messages 2026-09-01 23:09:53 +05:00
Zohaib Hassnain 930e7f9b71 docs(vector_store): note the milvus schema assumption 2026-09-01 23:09:53 +05:00
Zohaib Hassnain e335971dcd feat(vector_store): add milvus iter_all 2026-09-01 23:09:53 +05:00
Zohaib Hassnain 78682076d5 fix(vector_store): raise before yielding on a stalled weaviate cursor, extract v4 dict vectors, dedupe fallback ladder 2026-09-01 23:03:07 +05:00
Zohaib Hassnain 1227947be5 fix(vector_store): dedupe qdrant record conversion, don't abort iter_all on a live cursor with an empty page 2026-09-01 22:51:32 +05:00
Zohaib Hassnain b4a14d87f5 making it clean 2026-09-01 22:51:32 +05:00
Zohaib Hassnain 3bf89e523f fix(vector_store): raise instead of truncating when a qdrant scan cannot advance 2026-09-01 22:51:32 +05:00
Zohaib Hassnain 2b5b62bb8d fix(vector_store): drop qdrant migrate wiring, keep iter_all only
VectorStore cannot actually migrate to or from qdrant yet. _init_backend_store constructs QdrantStore without connecting or selecting a collection, so reads raise a Collection not initialized error, and the facade store_vectors dispatches only to add/add_vectors while QdrantStore exposes insert_vectors, so writes raise NotImplementedError.

Both are pre-existing facade gaps that nothing had exposed, since migrate previously only allowed faiss/sqlite/pgvector. Adding qdrant to the allowlist claimed support that does not work end to end, so it is removed along with the dimension inference that only fires for backends missing a .dimension attribute. Tracked separately; this PR keeps just the iter_all primitive.
2026-09-01 22:51:32 +05:00
Zohaib Hassnain 3a0f3f672a feat(vector_store): add iter_all enumeration and wire up qdrant migration 2026-09-01 22:51:32 +05:00
Mohd Kaif 18fb7c3ec0 Merge branch 'main' into fix/semantic-layer-basics-intro 2026-09-01 21:51:31 +05:30
taoche e6c05df33e docs(cookbook): index semantic layer capstone 2026-09-01 19:51:49 +08:00
taoche c20a46f026 docs(cookbook): preserve complete semantic layer RDF 2026-09-01 19:45:40 +08:00
taoche 0dbe9274eb docs(cookbook): install notebook 08 NER model 2026-09-01 19:45:39 +08:00
taocheandClaude Fable 5 0e7cef4677 docs(cookbook): move Semantic Layer Basics from Advanced to Introduction
Advanced chapter 09 labeled an introductory composition of
already-taught APIs as an enterprise semantic layer: TripletStore was
imported but never used, property_mappings stayed empty, mappings were
derived by fragile name matching (works_for never matched worksFor),
and the exported RDF was the original graph rather than an
ontology-aligned one.

Replace it with introduction/26_Semantic_Layer_Basics.ipynb, which
demonstrates the minimal semantic-layer composition honestly:

- build a small graph, generate an ontology (min_occurrences=1 so all
  demo classes are inferred, base_uri in the user's namespace)
- explicit entity-type, relationship-type, and property mappings read
  from the ontology's inferred_from metadata instead of name matching
- apply the mappings to produce an ontology-aligned graph, export it
  as Turtle, and note the file exporter's property projection
- store the aligned graph in the embedded Oxigraph TripletStore and
  answer a business question with one SPARQL query
- distinguish teaching mappings from governed production mappings and
  point to Advanced 13 as the production continuation

Advanced 13 gains a positioning note naming the new lesson as its
prerequisite; introduction/14_Ontology links forward to the new
lesson. All cells execute top to bottom (verified with the embedded
Oxigraph backend).

Closes #1325

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 15:56:49 +08:00
taocheandClaude Fable 5 5c366c6b7e docs(cookbook): make notebook 08 a real rerunnable end-to-end workflow
The first-knowledge-graph lesson read the parser output from a key it
never returns (content vs text), then masked the failure with
hard-coded entities, a manually assembled NetworkX graph, and an
uninvoked KGVisualizer; the final cell deleted the sample file, so
rerunning intermediate cells failed. Rework the notebook so every
stage consumes the previous stage's output:

- parse via parsed_document["text"] with an assertion that content
  was actually extracted
- real NERExtractor/RelationExtractor output replaces the simulated
  entities and wrong hard-coded offsets
- GraphBuilder builds the graph from actual relation endpoints via a
  mention-span -> graph-ID map (consistent with notebook 07)
- KGVisualizer.visualize_network renders the graph and saves HTML
- deletion moved to an explicit optional cleanup cell, so parsing and
  downstream cells stay rerunnable

Closes #1289

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 15:45:28 +08:00
Zohaib Hassnain bd1ba24b24 fix(vector_store): carry weaviate offset fallback across pages, raise on truncation 2026-08-31 13:22:23 +05:00
Zohaib Hassnain e8ff36f088 feat(vector_store): add weaviate iter_all 2026-08-31 03:01:37 +05:00
Zohaib Hassnain ec9e63e16f fix(vector_store): drop qdrant migrate wiring, keep iter_all only
VectorStore cannot actually migrate to or from qdrant yet. _init_backend_store constructs QdrantStore without connecting or selecting a collection, so reads raise a Collection not initialized error, and the facade store_vectors dispatches only to add/add_vectors while QdrantStore exposes insert_vectors, so writes raise NotImplementedError.

Both are pre-existing facade gaps that nothing had exposed, since migrate previously only allowed faiss/sqlite/pgvector. Adding qdrant to the allowlist claimed support that does not work end to end, so it is removed along with the dimension inference that only fires for backends missing a .dimension attribute. Tracked separately; this PR keeps just the iter_all primitive.
2026-08-31 03:00:56 +05:00
Zohaib Hassnain 274d5d1195 feat(vector_store): add iter_all enumeration and wire up qdrant migration 2026-08-31 02:34:26 +05:00
140 changed files with 6888 additions and 1858 deletions
+3 -3
View File
@@ -26,7 +26,7 @@ each file's own autogenerated header comment for its exact command).
| File | Used by | Installs |
| --- | --- | --- |
| `bootstrap.txt` | security.yml, security-scan.yml, benchmark.yml | pip, setuptools (upgrade before anything else) |
| `bootstrap.txt` | security-scan.yml, benchmark.yml | pip, setuptools (upgrade before anything else) |
| `pep517-build.txt` | ci.yml, benchmark.yml, Dockerfile | exact `[build-system] requires` from `pyproject.toml` (setuptools, wheel) - installed with `--no-build-isolation` before any `pip install -e .` / `pip install .`, since `--no-deps` alone doesn't stop pip's PEP 517 build isolation from fetching those two *unhashed* |
| `explorer-extra-py311.txt` | ci.yml | semantica's base deps + the `explorer` extra, resolved for python 3.11 |
| `explorer-extra-py313.txt` | Dockerfile | the same, resolved for python 3.13 (the image's actual interpreter) |
@@ -34,8 +34,8 @@ each file's own autogenerated header comment for its exact command).
| `uv-tool.txt` | ci.yml | uv, to verify requirements-ci.txt is current |
| `build-tools.txt` | ci.yml, release.yml | build, wheel |
| `twine.txt` | release.yml | twine |
| `pip-audit.txt` | security.yml | pip-audit |
| `security-scan-tools.txt` | security-scan.yml | safety, bandit, semgrep, jq |
| `pip-audit.txt` | security-scan.yml | pip-audit |
| `security-scan-tools.txt` | security-scan.yml | bandit, semgrep, jq |
| `base-deps.txt` | benchmark.yml | semantica's base deps (no extras) |
| `benchmark-extra.txt` | benchmark.yml | the benchmark-only libs (neo4j, pdfplumber, etc.) |
@@ -1,4 +1,3 @@
safety==3.8.1
bandit==1.9.4
semgrep==1.175.0
jq==1.12.0
+3 -308
View File
@@ -1,9 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile .github/requirements/security-scan-tools.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/security-scan-tools.txt
annotated-doc==0.0.5 \
--hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \
--hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb
# via typer
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
@@ -24,10 +20,6 @@ attrs==26.1.0 \
# jsonschema
# referencing
# semgrep
authlib==1.8.0 \
--hash=sha256:88aebbd9af6757e14e912d5dc007ae1dc1f3e27e3b2152ce7c552ee2c3b3c121 \
--hash=sha256:f3ecd5f1da737262fb53bf1a4d95c4ea1ad9dd509316587a255c99ab1838a4f0
# via safety
bandit==1.9.4 \
--hash=sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628 \
--hash=sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e
@@ -50,7 +42,6 @@ certifi==2026.7.22 \
# httpcore
# httpx
# requests
# safety
cffi==2.1.1 \
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
--hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \
@@ -332,19 +323,12 @@ click==8.4.2 \
--hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76
# via
# click-option-group
# nltk
# safety
# semgrep
# typer
# uvicorn
click-option-group==0.5.9 \
--hash=sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080 \
--hash=sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823
# via semgrep
cloudpickle==3.1.2 \
--hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \
--hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a
# via joblib
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
@@ -396,20 +380,7 @@ cryptography==50.0.1 \
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
# via
# authlib
# joserfc
# pyjwt
defusedxml==0.7.1 \
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
# via nltk
dparse==0.6.4 \
--hash=sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a \
--hash=sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57
# via
# safety
# safety-schemas
# via pyjwt
exceptiongroup==1.2.2 \
--hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \
--hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc
@@ -418,10 +389,6 @@ face==26.0.1 \
--hash=sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6 \
--hash=sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d
# via glom
filelock==3.32.4 \
--hash=sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd \
--hash=sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30
# via safety
glom==25.12.0 \
--hash=sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069 \
--hash=sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1
@@ -443,9 +410,7 @@ httpcore==1.0.9 \
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via
# mcp
# safety
# via mcp
httpx-sse==0.4.3 \
--hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \
--hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d
@@ -461,18 +426,6 @@ importlib-metadata==8.7.1 \
--hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \
--hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151
# via opentelemetry-api
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
# via safety
joblib==1.6.0 \
--hash=sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03 \
--hash=sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba
# via nltk
joserfc==1.7.5 \
--hash=sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159 \
--hash=sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea
# via authlib
jq==1.12.0 \
--hash=sha256:02112ca560f90c6b1ea31829bb7777fbc5b1f1d13f78b2c6ce5cefa8233cee7e \
--hash=sha256:067ea0d3ee2cd7f7ba9c5d5c1925b9b0f83e1869c97a65ef11d8d76bd91ece6e \
@@ -549,101 +502,6 @@ markdown-it-py==4.2.0 \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
# via rich
markupsafe==3.0.3 \
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
--hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \
--hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \
--hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \
--hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \
--hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \
--hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \
--hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \
--hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \
--hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \
--hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \
--hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \
--hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \
--hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
--hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \
--hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \
--hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \
--hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \
--hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \
--hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
--hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \
--hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \
--hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \
--hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \
--hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \
--hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \
--hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
--hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \
--hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
--hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \
--hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \
--hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \
--hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \
--hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \
--hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \
--hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \
--hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \
--hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \
--hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \
--hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \
--hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
--hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \
--hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
--hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \
--hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \
--hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \
--hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \
--hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
--hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \
--hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \
--hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \
--hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \
--hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \
--hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
--hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \
--hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \
--hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \
--hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \
--hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \
--hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \
--hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \
--hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \
--hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \
--hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
--hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \
--hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \
--hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \
--hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \
--hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \
--hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
--hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
--hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \
--hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \
--hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \
--hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \
--hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \
--hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \
--hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \
--hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \
--hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \
--hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \
--hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \
--hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \
--hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \
--hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \
--hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \
--hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \
--hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \
--hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50
# via jinja2
marshmallow==4.3.1 \
--hash=sha256:e65accfbe277546df92ed7996a678c90e063e9a7c2a2f5e03f7d0b90e3768c42 \
--hash=sha256:fb6b8048af08d4ab061610d5b7d3696a7e4c95337dbda880edb9f95812cabc20
# via safety
mcp==1.29.0 \
--hash=sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36 \
--hash=sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7
@@ -652,10 +510,6 @@ mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
# via markdown-it-py
nltk==3.10.3 \
--hash=sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4 \
--hash=sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c
# via safety
opentelemetry-api==1.37.0 \
--hash=sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7 \
--hash=sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47
@@ -716,10 +570,7 @@ packaging==26.3 \
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
# via
# dparse
# opentelemetry-instrumentation
# safety
# safety-schemas
# semgrep
peewee==3.19.0 \
--hash=sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417 \
@@ -749,8 +600,6 @@ pydantic==2.13.5 \
# via
# mcp
# pydantic-settings
# safety
# safety-schemas
pydantic-core==2.46.5 \
--hash=sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942 \
--hash=sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821 \
@@ -976,122 +825,6 @@ referencing==0.37.0 \
# via
# jsonschema
# jsonschema-specifications
regex==2026.8.31 \
--hash=sha256:0087dfa879bf01c5eb290848c7de22f717d8d4218a997080e63ae4813bc55104 \
--hash=sha256:026a7cd6c20a2a5bf3249a4a1c7f076af86b17188e2ffd17722e2ed24f433f9a \
--hash=sha256:073b9cb8c44e197a4d1d8b819a3329f6b20866d83d2700f78b9d33e1f1a75116 \
--hash=sha256:0abb98dd76a3ffe3b401fe93aadac135ecd6ba4a71d7b4be4a333de8d691e834 \
--hash=sha256:0bb6121dbf90c7de42610459398a81cbb90bc870e2cc003248f3f2b65d45f2b6 \
--hash=sha256:0ec77a1ce2350c74fe3821d1c6555107d41f6969c369f4ee197a10cec97632ec \
--hash=sha256:0ee80c5d20a62ae819f39a4f5b0c7f1dbbeb28186de6138840eb8c138e96f99e \
--hash=sha256:13f036b42889e8cad5f1ee2eadb48c656b2f44c5944035e0f697cb6ef81757ba \
--hash=sha256:15e9e862c6e905ef66ea5f019deb5ac5fdeebf8fc134ea4c7b5d5c2eb7bdcdd8 \
--hash=sha256:18ac65e72e8454343df30ca1d8a4ad604d3419b96e0ef8e2dc3a69642bb557b4 \
--hash=sha256:18c7e0348286f5073867d339d7cab60ed200b77b48d7a9be4edbcdc2c996a62b \
--hash=sha256:1930ade186f2b519fe9c4bdfd3a77410e469bd91423a995888b91f3beb12679b \
--hash=sha256:1e74e38c5a9ed3a70a0e0a89498eb664211b97c162d77b1131f37636779f36b4 \
--hash=sha256:222c906a555bdbd5322f15778bb2b4f238c26e1d52c9445f1e50f5e4452909b3 \
--hash=sha256:241c614ab811e29f2e67e2828404dd10a2dc675ec2c75a6017ec310fd09117b9 \
--hash=sha256:26a6ddc85198558b0c74b856f6440132d6f97248c22589bf52cf13df2fa44fdc \
--hash=sha256:2c5f4fc5463ac732ed49cb87ffdf2eab3d909a0df4100211ce4be3af1ad729cb \
--hash=sha256:2d28ad9d016ac681843b059ddca376b9ff833ec218c938035d925c8af44c6de7 \
--hash=sha256:34c8d36a5f70c16e3f406ae1c93a47ea4b2a40e29b02639cf41915b6fea5ce26 \
--hash=sha256:360c916117c988b120ba05aa106cd3c1aa7c0f4575a2db0d605d502b4ee334f4 \
--hash=sha256:38179404d70581402831c2c0de0c8ec3483d272beab2244095cb09b4eeb30ef7 \
--hash=sha256:3b3a020f2a43e9016624047ecc15cd0d472c11dfbe4d12fe030f574570467f35 \
--hash=sha256:3e139e792b016a614b9af4a43e036b259a8d32f751e9b5bda77b4af652ad8a17 \
--hash=sha256:40f4cdf6d38663cf8f56a52edde25ca6dbfb857f5a7d49cd7de3e0e1a0883bf4 \
--hash=sha256:4301de5a58a28fe95b6a865d3b97b5cea073bb4c6ad743211c32b004f32d5096 \
--hash=sha256:43581e1f0c1f624cb7e2e8195c443f6e3004fc376bd12d644cdc8e613c973323 \
--hash=sha256:453e9ffb310eede3f35303d7fb2e891382c98888d54f162e5a2e0174d1b75331 \
--hash=sha256:45537c0d48a84dd0f840ea7c308445ad1e83a04d28d6fc394d71ad24f9f55d2b \
--hash=sha256:45b0450d6ae52e2dfcdb5e58987b829ed5fc01b709fc5ff09a1e81ab13c5262a \
--hash=sha256:4c3ac1eec883a1d0fbba167e90bb1beb72289e765966b464f9b333090dfcae2e \
--hash=sha256:50a8677cca3d4df536776380161744d41ea5001f99cc2c4638e6b0625839fa61 \
--hash=sha256:520b14582a59f43ba9ba595938349e70238009f8deb8c35d5bbfe33e44fd0ba9 \
--hash=sha256:52f03cd8f259d8fb482a9e142ad17c8d1c931a69a7a932922f2222df05875d59 \
--hash=sha256:56f7516b00f720231b26fdcd41ac13cceab7a8c1c903b1ab98e173b0962a771d \
--hash=sha256:66df1812cf0fd5f0f59e4341c54247a15397354ee01231e1c2620b08032f3361 \
--hash=sha256:69c42c35758cf46c31d976d63c79fbbcb114fe192aa4c721c734204d0e3d7555 \
--hash=sha256:69fbc60c1c34790037cfd350dd1600436fdfea9ca221761c614fc5e633c7cabd \
--hash=sha256:6d5537087013e5ce841b9d0f19a564f18f33fa79489a7e8865f5a38ba2a4de7d \
--hash=sha256:6d5c9841dd924437e34d43bdbecbb31bc1a01c57bd974af8e1a0a98b0a7a731c \
--hash=sha256:6fcbf68a10dd6a564c737147e013e5dea6180c032e3c363629cf4d0f9d258752 \
--hash=sha256:7010dae7e7064ee091703cafce0143693e56931bb3d21a82483bb96ad8a37751 \
--hash=sha256:722c2dba81c28494dae77f06c0fd33f0ad215e1b7cc6e2b0f3bad36656413f84 \
--hash=sha256:75b888caf9469df3826876ae0e2f92f37e7bbad0455cfa028852d99815af9dd0 \
--hash=sha256:75cc2d43987040df8655c25b47c1d452c7d59b28df108d7b2c19a003d021601f \
--hash=sha256:79c7b6bd11620dc722a94e160965fa0e64124ca8841afaf9683d8fa659431cf5 \
--hash=sha256:7aa0688964b66ac50e2bf3b04b9e88bdab58fa5ea8130b403d72668df6f54cb9 \
--hash=sha256:7c06a4cbe33f8ad72c3bd9590630c07e55c7a7c581253d287b6ca645e2879051 \
--hash=sha256:7daf31011e73c16f8b824bc6a6992f0de8a9ae13133001d757668c852bcc6502 \
--hash=sha256:81391983ff052f922baebb0955a3be455d5731351b3a93e0638a8150bd44b8b5 \
--hash=sha256:8231dfdbb4baf59d35a10fc1115846bdcc43b30ab6ec8809ec807bfeea48a119 \
--hash=sha256:861a12bd9e8d3f26a9a36cc1b3426edacc70395b2e4f37c1402f40345e9c06db \
--hash=sha256:868d9113a744f2bfffa31197cadcda5b7fc3951a8621dd5899f9c0e4208ca196 \
--hash=sha256:897c2e301226fdfaf1a0c68219607718c40699df82dff09fd366b489b4c6e6d8 \
--hash=sha256:8b6bcc66372b493faa2b6153cd16a44db3bfa316411f81c4ba5d0ffa693244df \
--hash=sha256:8b7f1bdf1f36555fa0317f4f6cbbd5312f886edf9f2a41c8c298ffb9ad9f4a1a \
--hash=sha256:8d3e98b55372aa36b1e046a56a10f13cf0ef782ad6c86dbd64f3897c7e7a7a02 \
--hash=sha256:91a478b9a76b7f2b4cc704ec5f438041012ae7914716f8de0d56c11c9706203f \
--hash=sha256:9350fd448a6442ae27853ab9d4b8d5a0bcb6d7774923a4fdfddd104c4458b35f \
--hash=sha256:95c25f91b7c3f8121946e175a731eccf097dfeff065ab1204dbaad1ebf8ada6e \
--hash=sha256:976c265b3a42b806cf58afd3c5a64417e1bbd804289bf4abd38ea7395623531d \
--hash=sha256:98183eb943ebcd2e89fd9fcb4103bfafc5369cff9479561a5c96de2fe90cae68 \
--hash=sha256:98381539ee2dd88794f3ce6e40166f59b93e6e3ee9cd27dea9f2dd6b857f3dbc \
--hash=sha256:9a991b561615498877b042b13a788cc2f33c99087a9540627c397037c58ae795 \
--hash=sha256:9acbc6901bea11ad2f21d32b0790cbe2cb0194b521ea239231e1ee9627efd585 \
--hash=sha256:9b9e48a4ae2378c7bb29df0cbe2426cf0929ddbbae5819225c1fe133e6bb368d \
--hash=sha256:9fe2540d8da1bbf12f7c1b909a9ae47c2b343fa2a2084280c21ead1c9fb0e6f7 \
--hash=sha256:a1c9cd392daa08d3a3d5b663443a08071f4efbc1476f902142d51a229c60e852 \
--hash=sha256:a54f6b1b418e40b908ff9b9dd3e5fa638a2bd1bbe6e24180dc097c92b1deed0f \
--hash=sha256:a55bfb3914b760d5103d313a1053d301b2776f4677eb7f4d09f6420c625d97dd \
--hash=sha256:a679703a46574dcfbbae42acbc538d37653fa78dd2a3826f27c2dab386ea194d \
--hash=sha256:a75efe8109ebfaa5574aff49882fe471287ecb7959d96d29660cec937e5af1ce \
--hash=sha256:aac83eab8d47e3c290b9d30a34f94e3d888b7dd42f7cc45b8d204154cec3017b \
--hash=sha256:abd6b935adcd6c19733f20080a85972c6199cc9599dd8d16c9bbd1bbada569d8 \
--hash=sha256:aea17d86e7581e589fb8c43b70dc5f6588b1897390442536697a551bc66e2fd6 \
--hash=sha256:b40aee7f8df89d239943a932bfb53809f6b2c2ad53c049ee329100a54d3e1cfd \
--hash=sha256:b94165c6b98404ca40838852febd60df4fa6380dc0898f28dedaf5fca638e7ca \
--hash=sha256:bb1ca9e722c7270fb4267abee42cf8cfa97bc8e361b73839a50f00fcd2b76636 \
--hash=sha256:bb392c55059edb1bda593ee12218f5198a337535ff5e52f806c224c57b98716b \
--hash=sha256:bc00f39b7201fca5a15f12580f9dfb84b226323ad24043ec71b1132b5dbab711 \
--hash=sha256:bdbc6e87c9868ab2e7f29eed32b04583420df1b9b19e718f212e140c01f8b026 \
--hash=sha256:c01865f6a72c776064e4f58030e59f925e5fef32066aab3cb1a97be191f7bdd1 \
--hash=sha256:c72238cc48cd020f415e9dd3cba6c6b1af559d613358d282f7957cf61f0bcf6b \
--hash=sha256:c7ffcdf6fe74cedd4e36a9de2fb072b526a978e9b2d4fd2431edca96d80a67cd \
--hash=sha256:c9ba0b56ca6547e238323452178e5d9889886c99cdd17a4333d026f3c84471c5 \
--hash=sha256:c9c7a13d018f4f84503986564a543c2f7657a4bec4895f2c2cc584fb09d7429b \
--hash=sha256:caa959da9bb21394131eaf5c57698b47926ebada98c6796cfb4e754a52de001f \
--hash=sha256:cf427a3bebc873a2601601fc5e8453d1396b52d694ad65788fa2b22fe7b0f920 \
--hash=sha256:cf6c32d2a6bdaac692915ab81f28b62525d937abeac80149260db2c904a5df97 \
--hash=sha256:d27a3bdd19aa00974ac53ba14faea80ecef412f2d957c0071a869d7baea820f4 \
--hash=sha256:d59beef8054a851b2a3f42f56f94770981973699ab4c7f0b5f6984c26205b76c \
--hash=sha256:d84db4aaf4b5c5c4d512ce06420850c909865fa7d6223081dc8e9dbde7a83754 \
--hash=sha256:d9759f4cc91880cfafdb11b7b2bc83e34f2f16d103fd94f936d804cbfdb9c1aa \
--hash=sha256:dacc364aa1c06cb3fffb1705ff313cb3622c94d8c248f29e57bac2acadd77bf7 \
--hash=sha256:dbed5cea80c5a67c3f95f16d011d68174eb81a5efccf87a3ad0822b79d74baae \
--hash=sha256:deab998bd9314f7e93f519d3f62f1fd9e83a2db654f579cadac3968fbc1b5976 \
--hash=sha256:def853717c37661f59942c76ad06e060630f6e297257bcfb6f203d2daf497d41 \
--hash=sha256:dfc722cb60e40e6fefa483a7583baa4af55ac87babb5ecfc8989e54e5e182d1d \
--hash=sha256:e169081d7ae955f4bd1a590a7ec29f1032eae6889539cf7047bd0f7b09daedc9 \
--hash=sha256:e5578ad134fa81286622faff397650cfa2249f640af783b8c2abbae1c70dacdd \
--hash=sha256:e67af1dcebc0663cd90253cfb4653f991d0995160ec9ca3132924d7956e17c6e \
--hash=sha256:ebe363e5c252dc9011b0380c9b0b8ef559573dcc325ec8f3165129d21af10b63 \
--hash=sha256:ec9a66ed2ed23611dcfaa87a860f1511a56ded56f01dd161eeebddb6e25590c3 \
--hash=sha256:ed723dc78dd6f676f38083bd86194dbe91befd8c3ecb9cd2f47147bfe7d26dd1 \
--hash=sha256:ed865d560365bb3797e4e05dcbd83fb7a045893cc54f0d72588f90eb05c68fee \
--hash=sha256:efefb4c85414b6e4be19a53f90d58b573f551b7e4d1dc1e566f7030b6ca4fa8f \
--hash=sha256:f078f774d094ea32302163419141fda36176b954069956296406ae1cf4b00222 \
--hash=sha256:f2ecb87363dd9e13fa9def0a5c7a61ef5ccc952c08b99672e6f95fdb2463ccd9 \
--hash=sha256:f59d36c5356ca6ff79b1a91ef39845c0dd71eeee6b98d71cd0972307eba77260 \
--hash=sha256:f696d058d233923b7259d2d963f92b9cf2906063820f27cbd4085529d78861c3 \
--hash=sha256:f69c363342b81fce87f2e9dafd05ec041b67ee3b74c08ee9d2be5aeab8d484da \
--hash=sha256:f8b784a28492f4020dc90ef6b6d0bb3ca591cb1331de6362968308ed5243b550 \
--hash=sha256:f9594423bace86d47d080ae92329315b977fe6466ac998e36a88563c9c6d0259 \
--hash=sha256:fb7df717e6c9f2b59aebdf558242da87b2b5cd5961b9469efe8f01762dfe4cc1 \
--hash=sha256:ff7cc959f3535028c03c201bbe6703ce1cb5051164f08bca9f814e04333fbb48
# via nltk
requests==2.34.2 \
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
@@ -1104,7 +837,6 @@ rich==15.0.0 \
# via
# bandit
# semgrep
# typer
rpds-py==2026.6.3 \
--hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \
--hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \
@@ -1228,10 +960,7 @@ rpds-py==2026.6.3 \
ruamel-yaml==0.19.1 \
--hash=sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93 \
--hash=sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993
# via
# safety
# safety-schemas
# semgrep
# via semgrep
ruamel-yaml-clib==0.2.15 \
--hash=sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490 \
--hash=sha256:04d21dc9c57d9608225da28285900762befbb0165ae48482c15d8d4989d4af14 \
@@ -1295,14 +1024,6 @@ ruamel-yaml-clib==0.2.15 \
--hash=sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51 \
--hash=sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f
# via semgrep
safety==3.8.1 \
--hash=sha256:953c1c3c60c873f53a6cc250b2a9c4b38bb6ef45f0625990e43f20bff916c965 \
--hash=sha256:e646123b976bbb6707cfaacae8c926e2f886b744a60e0f410e8610a3a4eaf7be
# via -r .github/requirements/security-scan-tools.in
safety-schemas==0.0.16 \
--hash=sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e \
--hash=sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44
# via safety
semantic-version==2.10.0 \
--hash=sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c \
--hash=sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177
@@ -1317,10 +1038,6 @@ semgrep==1.175.0 \
--hash=sha256:e8b14c91558f765b9dd155a99b0071bfe64f61577cda8eb4964132155232c1af \
--hash=sha256:e8ecd7ee8ef1033c9635111c6e162e778834d61416968c5c1c5e7b7fba35c34e
# via -r .github/requirements/security-scan-tools.in
shellingham==1.5.4 \
--hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \
--hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de
# via typer
sse-starlette==3.4.8 \
--hash=sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d \
--hash=sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c
@@ -1335,10 +1052,6 @@ stevedore==5.9.1 \
--hash=sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf \
--hash=sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf
# via bandit
tenacity==9.1.4 \
--hash=sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55 \
--hash=sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a
# via safety
tomli==2.4.1 \
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
@@ -1388,22 +1101,6 @@ tomli==2.4.1 \
--hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
--hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
# via semgrep
tomlkit==0.15.1 \
--hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \
--hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97
# via safety
tqdm==4.70.0 \
--hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \
--hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953
# via nltk
truststore==0.10.4 \
--hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \
--hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981
# via safety
typer==0.25.1 \
--hash=sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 \
--hash=sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc
# via safety
typing-extensions==4.16.0 \
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
@@ -1417,8 +1114,6 @@ typing-extensions==4.16.0 \
# pydantic
# pydantic-core
# referencing
# safety
# safety-schemas
# semgrep
# starlette
# typing-inspection
+54 -4
View File
@@ -12,13 +12,63 @@ on:
- '**/*.md'
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
- 'docs_check.py'
- '**/*.md'
jobs:
# Detect whether this PR touches any source files (non-docs/non-markdown).
# The result drives the `build` job's `if:` condition so that:
# - docs-only PRs: `build` is skipped (satisfies the required check).
# - code PRs: `build` runs exactly as before.
# Push events (to main) keep their own paths-ignore above and never reach
# this job, so the push optimization is unaffected.
changes:
runs-on: ubuntu-latest
# Only needed for pull_request events; push events are pre-filtered above.
if: github.event_name == 'pull_request'
outputs:
src: ${{ steps.filter.outputs.src }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Fetch enough history to compute the merge base against the PR base.
fetch-depth: 0
- name: Check for source changes
id: filter
run: |
# List files changed in this PR relative to the true merge base.
# Using three-dot merge-base diff so changes on the base branch that
# are not part of this PR do not appear in the file list.
# If every changed file matches docs/** or *.md (any depth) or
# docs_check.py, this is a docs-only PR and src=false; otherwise
# src=true.
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
MERGE_BASE=$(git merge-base "$BASE" "$HEAD")
CHANGED=$(git diff --name-only "$MERGE_BASE" "$HEAD")
echo "Changed files:"
echo "$CHANGED"
NON_DOCS=$(echo "$CHANGED" | grep -Ev '^(docs/|docs_check\.py|.*\.md$)' || true)
if [ -n "$NON_DOCS" ]; then
echo "src=true" >> "$GITHUB_OUTPUT"
else
echo "src=false" >> "$GITHUB_OUTPUT"
fi
build:
needs: [changes]
# For pull_request events:
# - skip only when changes ran successfully and explicitly set src=false
# (i.e. a confirmed docs-only PR).
# - run when changes succeeded with src=true (source changes present).
# - run when changes failed or was cancelled (fail-closed: missing output
# must not silently skip the build).
# For push/non-PR events: changes is skipped; always() prevents the build
# from being skipped due to a skipped needs dependency.
if: >-
always() && (
github.event_name != 'pull_request' ||
needs.changes.result != 'success' ||
needs.changes.outputs.src == 'true'
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+1 -1
View File
@@ -65,4 +65,4 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5
+198 -80
View File
@@ -3,6 +3,7 @@ name: Security Scan
on:
schedule:
- cron: '30 1 * * 1,4' # Mon/Thu 7 AM IST
workflow_dispatch:
push:
branches: [main]
paths-ignore:
@@ -12,17 +13,65 @@ on:
- '**/*.md'
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
- 'mkdocs.yml'
- 'requirements-docs.txt'
- '**/*.md'
permissions:
contents: read
jobs:
# Detect whether this PR touches any source files (non-docs/non-markdown).
# The result drives the `security-scan` job's `if:` condition so that:
# - docs-only PRs: `security-scan` is skipped (satisfies the required check).
# - code PRs: the full scan runs exactly as before.
# Schedule and workflow_dispatch runs always skip this job and run the scan
# unconditionally (the security-scan job's if: accounts for that below).
# Push events (to main) keep their own paths-ignore above.
changes:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
outputs:
src: ${{ steps.filter.outputs.src }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Check for source changes
id: filter
run: |
# List files changed in this PR relative to the true merge base.
# Using three-dot merge-base diff so changes on the base branch that
# are not part of this PR do not appear in the file list.
# If every changed file matches the docs/markdown paths-ignore list
# (at any directory depth), this is a docs-only PR and src=false;
# otherwise src=true.
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
MERGE_BASE=$(git merge-base "$BASE" "$HEAD")
CHANGED=$(git diff --name-only "$MERGE_BASE" "$HEAD")
echo "Changed files:"
echo "$CHANGED"
NON_DOCS=$(echo "$CHANGED" | grep -Ev '^(docs/|mkdocs\.yml$|requirements-docs\.txt$|.*\.md$)' || true)
if [ -n "$NON_DOCS" ]; then
echo "src=true" >> "$GITHUB_OUTPUT"
else
echo "src=false" >> "$GITHUB_OUTPUT"
fi
security-scan:
# For pull_request events:
# - skip only when changes ran successfully and explicitly set src=false
# (i.e. a confirmed docs-only PR).
# - run when changes succeeded with src=true (source changes present).
# - run when changes failed or was cancelled (fail-closed: missing output
# must not silently skip the security scan).
# For schedule/workflow_dispatch/push: changes is skipped; always() ensures
# the scan still runs unconditionally for those triggers.
needs: [changes]
if: >-
always() && (
github.event_name != 'pull_request' ||
needs.changes.result != 'success' ||
needs.changes.outputs.src == 'true'
)
runs-on: ubuntu-latest
permissions:
contents: read
@@ -45,85 +94,100 @@ jobs:
- name: Install dependencies
run: |
pip install -r .github/requirements/bootstrap.txt --require-hashes
# Install the pinned dependency set FIRST so Safety scans Semantica's
# exact CI/release dependency tree (requirements-ci.txt is generated
# from pyproject.toml extras, so this covers the project's real deps).
# Install the pinned dependency set FIRST so pip-audit scans
# Semantica's exact CI/release dependency tree (requirements-ci.txt
# is generated from pyproject.toml extras, so this covers the
# project's real deps).
pip install -r requirements-ci.txt --require-hashes
# Tooling AFTER the pinned set: installing safety/bandit/semgrep/jq
# first lets the pinned requirements overwrite their transitive deps
# (e.g. rich), which breaks the safety CLI at runtime.
# Tooling AFTER the pinned set: installing it first would let the
# pinned requirements overwrite the tooling's own transitive deps.
pip install -r .github/requirements/pip-audit.txt --require-hashes
pip install -r .github/requirements/security-scan-tools.txt --require-hashes
- name: Run Safety Check (Package Vulnerabilities)
- name: Run pip-audit (Package Vulnerabilities)
continue-on-error: true
run: |
# NOTE: Safety 3.x repurposed --output to select a console format
# (json/text/screen/...), not a file path. Writing JSON to a file
# now requires --save-json; the previous `--output safety-report.json`
# usage was silently invalid and never produced a report.
#
# Scan requirements-ci.txt directly instead of the installed environment
# to avoid crashes from packages like cuda-toolkit that Safety cannot
# parse. This also ensures we're auditing the declared dependency tree
# rather than transitive dependencies of the security tooling itself.
safety check --file requirements-ci.txt --save-json safety-report.json || true
# Keep publishing reports and the PR comment even when the audit
# gate fails. The final gate below preserves the failure status.
echo 'AUDIT_SCAN_STATUS=failed' >> "$GITHUB_ENV"
# Guard 1: fail loudly if Safety exited before writing a report at all
# (network error, API auth failure, tool crash). Without this check a
# missing or empty file causes jq to fall back to "0", making a broken
# Same dependency tree Safety used to scan, and the same tool and
# invocation already proven reliable in security.yml.
pip-audit -r requirements-ci.txt --format=json --output=pip-audit-report.json || true
# Guard 1: fail loudly if pip-audit exited before writing a report
# at all (network error, tool crash). Without this check a missing
# or empty file causes jq to fall back to "0", making a broken
# scanner indistinguishable from a clean scan.
if [ ! -s safety-report.json ]; then
echo "::error::Safety scan produced no report (safety-report.json is missing or empty). Treating as failure — check for network errors, API auth failures, or Safety crashes in the logs above."
if [ ! -s pip-audit-report.json ]; then
echo "::error::pip-audit produced no report (pip-audit-report.json is missing or empty). Treating as failure — check for network errors or pip-audit crashes in the logs above."
exit 1
fi
# Guard 2: fail closed when the report doesn't have the shape the
# checks below assume: a non-empty dependencies array, each entry
# either carrying an array-valued vulns field or being a dependency
# pip-audit couldn't resolve/audit, which it reports as
# {"name": ..., "skip_reason": ...} with no vulns field at all
# (see pip_audit._format.json.JsonFormat._format_dep). That's a
# normal, documented report shape, not a malformed one — treating
# it as invalid would fail the whole job over a single unauditable
# package, the same kind of scan-unrelated CI break this migration
# away from Safety was meant to fix.
if ! jq -e '
(.dependencies | type == "array" and length > 0)
and all(.dependencies[]; type == "object" and ((.vulns | type == "array") or (.skip_reason | type == "string")))
' pip-audit-report.json >/dev/null 2>&1; then
echo "::error::pip-audit report has an invalid dependency structure. Expected a non-empty dependencies array where every entry has either a vulns array or a skip_reason. Treating as failure."
exit 1
fi
echo "Checking for package vulnerabilities..."
# Guard 2 above already confirmed pip-audit-report.json is valid
# JSON with a well-shaped dependencies array, so this count is
# always a plain non-negative integer.
SKIPPED=$(jq '[.dependencies[] | select(has("skip_reason"))] | length' pip-audit-report.json)
if [ "$SKIPPED" -gt 0 ]; then
echo "⚠️ pip-audit could not audit $SKIPPED dependencies (see pip-audit-report.json for skip_reason):"
jq -r '.dependencies[] | select(has("skip_reason")) | " - \(.name): \(.skip_reason)"' pip-audit-report.json
fi
# Vulnerability IDs reviewed and accepted as non-actionable for this
# project. Filtered out here with jq rather than passed to Safety's
# own --ignore flag: --ignore crashes ("Unhandled exception happened:
# 'cuda-toolkit'") when it has to apply itself against a live-matched
# vulnerability for cuda-toolkit, apparently the same class of
# unguarded dependency-graph lookup that broke the plain environment
# scan (see git history on this file). The un-ignored scan above is
# the one path confirmed - by an actual CI run - not to crash even
# with a live cuda-toolkit match, so all filtering happens after the
# fact in jq instead of inside Safety.
#
# - SFTY-20260120-40557 (CVE-2025-33228): cuda-toolkit<13.1.0. torch
# 2.13.0 (latest available; no newer release exists) hard-pins
# cuda-toolkit[cublas,cudart,cufft,cufile,cupti,curand,cusolver,
# cusparse,nvjitlink,nvrtc,nvtx]==13.0.3 on Linux - not a version we
# control. The CVE is OS command injection in NVIDIA Nsight
# Systems' gfx_hotspot recipe (process_nsys_rep_cli.py), requiring
# manual invocation with an attacker-supplied string; unreachable
# from Semantica, and Nsight Systems isn't among the extras torch
# requests above. Re-evaluate once torch pins a patched
# cuda-toolkit.
IGNORED_VULN_IDS="SFTY-20260120-40557"
# project. Empty for now: pip-audit's OSV-backed database doesn't
# currently carry either of the findings Safety used to flag here
# (cuda-toolkit CVE-2025-33228, torchvision CVE-2026-65918), so
# there's nothing to exclude. Left in place so a future finding can
# be added the same way without restructuring this step - see git
# history on this file for the reasoning behind past entries.
IGNORED_VULN_IDS=""
# Exported so the "Comment PR with Security Results" step below can
# apply the same exclusion list to the raw report - it reads
# safety-report.json independently in JS, so without this the PR
# comment would show the accepted CVE as a live finding even though
# this gate correctly treats it as non-actionable.
# pip-audit-report.json independently in JS, so without this the PR
# comment would show an accepted finding as live even though this
# gate correctly treats it as non-actionable.
echo "IGNORED_VULN_IDS=$IGNORED_VULN_IDS" >> "$GITHUB_ENV"
# No []? / || echo "0" fallback on a missing/null "vulnerabilities"
# key: iterating over null raises inside jq, leaving VULNS empty, so
# guard 2 below catches it rather than silently treating a broken
# report as zero.
# No []? / || echo "0" fallback: if jq fails (malformed JSON) VULNS
# will be empty or "null" so Guard 3 below catches it rather than
# silently treating the broken report as zero.
# `.vulns // []` guards against skipped dependencies, which carry
# no vulns field at all (see the skip_reason handling above) -
# without the fallback, iterating `null[]` raises inside jq and
# this whole computation silently evaluates to empty.
VULNS=$(jq --arg ignored "$IGNORED_VULN_IDS" '
($ignored | split(",")) as $ignore_list
| [.vulnerabilities[] | select(.vulnerability_id as $id | ($ignore_list | index($id)) | not)]
($ignored | split(",") | map(select(length > 0))) as $ignore_list
| [.dependencies[] | (.vulns // [])[] | select(.id as $id | ($ignore_list | index($id)) | not)]
| length
' safety-report.json 2>/dev/null)
' pip-audit-report.json 2>/dev/null)
# Guard 2: ensure VULNS is a non-negative integer before the -gt
# Guard 3: ensure VULNS is a non-negative integer before the -gt
# comparison. "null" (missing/null key) or "" (jq parse failure) would
# cause bash's -gt to throw an arithmetic error and fall through to the
# success branch — the same silent-pass bug as a missing file.
if ! [[ "$VULNS" =~ ^[0-9]+$ ]]; then
echo "::error::Safety report exists but 'vulnerabilities' is missing or non-numeric (got: '${VULNS}'). The report may be malformed or Safety may have written an error-only JSON. Treating as failure."
echo "::error::pip-audit report exists but dependency vulnerabilities are missing or non-numeric (got: '${VULNS}'). The report may be malformed or contain an error-only JSON response. Treating as failure."
exit 1
fi
@@ -133,15 +197,17 @@ jobs:
echo ""
echo "Vulnerability details:"
jq --arg ignored "$IGNORED_VULN_IDS" -r '
($ignored | split(",")) as $ignore_list
| .vulnerabilities[] | select(.vulnerability_id as $id | ($ignore_list | index($id)) | not)
| "- \(.package_name)==\(.analyzed_version): \(.vulnerability_id) (\(.CVE // "no CVE assigned"))"
' safety-report.json || true
($ignored | split(",") | map(select(length > 0))) as $ignore_list
| .dependencies[] as $dependency
| ($dependency.vulns // [])[] | select(.id as $id | ($ignore_list | index($id)) | not)
| "- \($dependency.name)==\($dependency.version): \(.id)"
' pip-audit-report.json || true
exit 1
else
echo "✅ No actionable security vulnerabilities found (ignored: $IGNORED_VULN_IDS)"
echo "✅ No actionable security vulnerabilities found${IGNORED_VULN_IDS:+ (ignored: $IGNORED_VULN_IDS)}"
echo 'AUDIT_SCAN_STATUS=passed' >> "$GITHUB_ENV"
fi
- name: Run Bandit (Code Security Linter)
run: |
bandit -r semantica/ -f json -o bandit-report.json || true
@@ -179,17 +245,18 @@ jobs:
fi
- name: Upload Security Reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: security-reports
retention-days: 14
path: |
safety-report.json
pip-audit-report.json
bandit-report.json
semgrep-report.json
- name: Comment PR with Security Results
if: github.event_name == 'pull_request'
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
@@ -212,6 +279,12 @@ jobs:
}
const items = parse(data);
if (items === null) {
return [
'### ' + title,
'⚠️ Invalid report structure in ' + reportPath + ' — check the job logs.',
].join('\n');
}
if (items.length === 0) {
return [`### ${title}`, `✅ No findings.`].join('\n');
}
@@ -231,23 +304,60 @@ jobs:
// Mirrors the shell step's own IGNORED_VULN_IDS (passed through
// $GITHUB_ENV) so an accepted, non-actionable CVE that the CI
// gate already excluded doesn't reappear here as a live finding -
// this reads the same raw, unfiltered safety-report.json.
// this reads the same raw, unfiltered pip-audit-report.json.
const ignoredVulnIds = (process.env.IGNORED_VULN_IDS || '')
.split(',')
.map((id) => id.trim())
.filter(Boolean);
const safetySection = renderSection(
'Safety — dependency vulnerabilities',
'safety-report.json',
(data) => (data.vulnerabilities || [])
.filter((v) => !ignoredVulnIds.includes(v.vulnerability_id))
.map(
(v) => `- \`${v.package_name}==${v.analyzed_version}\`: ${v.vulnerability_id}` +
(v.CVE ? ` (${v.CVE})` : '') + ` — ${v.advisory || 'no advisory text'}`
)
// A dependency pip-audit couldn't resolve/audit is reported as
// {"name": ..., "skip_reason": ...} with no vulns field at all
// (see pip_audit._format.json.JsonFormat._format_dep) - that's a
// normal report shape, not a malformed one, so it must not be
// treated as an invalid dependency below.
const isSkipped = (dependency) => typeof dependency.skip_reason === 'string';
let skippedDeps = [];
try {
const auditData = JSON.parse(fs.readFileSync('pip-audit-report.json', 'utf8'));
skippedDeps = (auditData.dependencies || []).filter(
(dependency) => dependency && typeof dependency === 'object' && isSkipped(dependency)
);
} catch (e) {
// Unreadable/unparseable report - renderSection's own
// report-missing branch below surfaces this.
}
const pipAuditSection = renderSection(
'pip-audit — dependency vulnerabilities',
'pip-audit-report.json',
(data) => {
if (
!Array.isArray(data.dependencies) ||
data.dependencies.length === 0 ||
data.dependencies.some(
(dependency) =>
!dependency ||
typeof dependency !== 'object' ||
(!Array.isArray(dependency.vulns) && !isSkipped(dependency))
)
) {
return null;
}
return data.dependencies.flatMap((dependency) =>
(dependency.vulns || [])
.filter((vulnerability) => !ignoredVulnIds.includes(vulnerability.id))
.map(
(vulnerability) => `- \`${dependency.name}==${dependency.version}\`: ${vulnerability.id}` +
(vulnerability.fix_versions?.length ? ` (fixed by ${vulnerability.fix_versions.join(', ')})` : '')
)
);
}
) + (ignoredVulnIds.length
? `\n\n_Excluded as accepted, non-actionable findings: ${ignoredVulnIds.join(', ')} — see the workflow file's inline comments for why._`
: '') + (skippedDeps.length
? `\n\n_Could not be audited: ${skippedDeps.map((d) => `\`${d.name}\` (${d.skip_reason})`).join(', ')}_`
: '');
const banditSection = renderSection(
@@ -269,7 +379,7 @@ jobs:
const comment = [
'# 🔒 Security Scan Results',
'',
safetySection,
pipAuditSection,
'',
banditSection,
'',
@@ -279,7 +389,7 @@ jobs:
'',
'*This security scan runs automatically on source-code PRs and bi-weekly (skipped for doc/markdown-only changes).*',
'',
'📊 **Security Policy**: CI fails on Safety vulnerabilities and Bandit HIGH-severity findings. Semgrep findings above are informational and do not block merge.',
'📊 **Security Policy**: CI fails on pip-audit vulnerabilities and Bandit HIGH-severity findings. Semgrep findings above are informational and do not block merge.',
].join('\n');
try {
@@ -294,3 +404,11 @@ jobs:
console.log('⚠️ Could not post security comment:', error.message);
console.log('📋 Security scan results saved to artifacts');
}
- name: Enforce Audit Gate
if: always()
run: |
if [ "${AUDIT_SCAN_STATUS:-failed}" != "passed" ]; then
echo "::error::pip-audit scan failed. See the pip-audit output and uploaded reports above."
exit 1
fi
-42
View File
@@ -1,42 +0,0 @@
name: Security
on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:
pull_request:
branches: [main]
paths:
- 'pyproject.toml'
- 'requirements-ci.txt'
- '.github/workflows/security.yml'
permissions:
contents: read
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: '3.11'
# Upgrade first: actions/setup-python's baked-in setuptools has been
# behind known-vulnerable floors before (e.g. PYSEC-2026-3447 /
# setuptools 75.1.0), so don't trust the preinstalled one.
- run: pip install -r .github/requirements/bootstrap.txt --require-hashes
# Audit the pinned dependency set (requirements-ci.txt is compiled from
# pyproject.toml with --extra all — the same coverage as the [all]
# extra, minus the Linux-only gpu set — so this keeps scan parity with
# CI/release builds without a time-dependent resolution). This is the
# fix for PYSEC-2024-38 (#869): the bare-env job never had fastapi or
# python-multipart installed to look at.
- run: pip install -r requirements-ci.txt --require-hashes
# PR runs gate on findings, since they're scoped to actual
# pyproject.toml changes under review. The schedule/workflow_dispatch
# runs stay non-blocking until a full pass over pre-existing findings
# across the whole [all] tree has been done.
- run: pip install -r .github/requirements/pip-audit.txt --require-hashes
- run: pip-audit -r requirements-ci.txt
continue-on-error: ${{ github.event_name != 'pull_request' }}
BIN
View File
Binary file not shown.
+21
View File
@@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Salesforce ingestor** (#1240) by @Sameer6305
- New `SalesforceConnector` / `SalesforceData` / `SalesforceIngestor` (`semantica.ingest`, lazy export), following the same Connector + Data + Ingestor pattern already used for Snowflake/Databricks/SAP
- Auth covers both landscapes Salesforce actually uses: username + password + security token (SOAP login), session_id + instance_url (reusing an existing session), and username + consumer_key + private key (JWT Bearer); production and sandbox are selected via `domain`, and credentials can come from environment variables. Credential material is never intentionally written to logs, exceptions, or `repr()`
- `ingest_sobject()`, `ingest_query()`, `list_sobjects()`, `get_sobject_schema()`, `export_as_documents()` against standard sObjects, custom objects (`__c`), custom metadata (`__mdt`), platform events (`__e`), namespaced objects, and relationship-field traversal (e.g. `Owner.Name`); pagination follows `nextRecordsUrl`/`query_more()` and stops once a caller's `limit` is satisfied
- New `pip install semantica[db-salesforce]` extra (`simple-salesforce>=1.12.0`)
- New `tests/test_salesforce_ingestor.py`
- Docs: `docs/integrations/salesforce.md`
- **`ErasureCoordinator` completes the erasure workflow `purge_node()` only starts — the graph node was removed while the same content survived verbatim in `AgentMemory` and as an embedding** (closes #1018) by @pravit-amp
- New `semantica/context/erasure.py`, exporting `ErasureCoordinator` and `ErasureReceipt` from `semantica.context`. `purge_node()`/`purge_edge()` (#957) are graph-scope by design and their changelog entry documents this gap explicitly; the changelog also names GDPR Article 17 as the motivation, and an Article 17 erasure that removes the node while the content stays retrievable by similarity search is not an erasure — it is worse than not offering one, because `purge_node()` returns `True` and writes a tombstone attesting the content is gone
- The coordinator **composes** the existing public APIs — nothing in `context_graph.py` or `agent_memory.py` changes behaviorally, and `ContextGraph` keeps its documented graph-scope contract rather than acquiring references to `AgentMemory`/`vector_store` that would invert the dependency
@@ -151,6 +159,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Also fixed, on the JSON-LD paths**: the first fix covered the Turtle, N-Triples and RDF/XML serializers, and left both JSON-LD writers interpolating the entity's own text into `f"semantica:entity/{text}"` and the endpoints into `f"semantica:rel/{source}_{target}"`. Three consequences, all live in 0.6.5: an entity whose text contained a space produced an invalid IRI, and a JSON-LD parser dropped that node in full rather than reporting it, so the entity disappeared from the export; every relationship carrying `source`/`target` rather than `source_id`/`target_id` minted the identical `semantica:rel/_`, collapsing all of them onto one node whose types and endpoints merged; and the JSON-LD `@id` disagreed with the Turtle IRI for the same entity, so the two serializations of one knowledge graph were two different graphs. Both JSON-LD writers now use `mint_entity_iri`/`mint_relationship_iri`, and `JSONExporter.export_entities`/`export_relationships` declare the `semantica` prefix their `@context` was already writing `semantica:entities` against — without it a processor reads that as an IRI in the scheme `semantica`, which is the original #1101 defect on a third path
- `tests/export/test_jsonld_iri_minting.py` parses each export with a real JSON-LD processor and asserts the entity survives, the relationships stay distinct, no term expands into the `semantica` scheme, and the JSON-LD `@id` equals the Turtle IRI
- 236 export and ontology tests pass
- **`semantica.evals` runner gains per-metric objectives** (#1091)
- `evaluate()` now accepts `config={"<evaluator>": {"objective": {"direction": "maximize"|"minimize", "threshold": X}}}` to override the evaluator's default pass verdict with a threshold; `{"objective": {"expect": bool}}` expresses a Boolean expectation
- `minimize` requires a `threshold` — omitting it or setting it to `None` raises `ValueError`; `maximize` without a threshold is a no-op (the evaluator's own verdict stands); `expect` cannot be combined with `direction`/`threshold`; invalid config raises `ValueError` before any evaluator runs
- Error metrics are never affected by objectives (error wins over fail)
- Backward compatible: no `objective` key → existing behavior unchanged
- New tests in `tests/evals/test_runner.py::TestObjective`
- **`semantica.evals` is now a fully implemented evaluation module** (was a "Coming Soon" stub in the package layout)
- `evaluate(cases, evaluators, config=None, target_fn=None)` runner with per-case `pass`/`fail`/`error` status and an aggregate `pass_rate`, using a registry of named evaluators (`list_evaluators()`)
- 10 built-in evaluators: `exact_match`, `regex_match`, `numeric_range`, `temporal_range`, `length_range`, `keyword_check`, `levenshtein` (edit-distance similarity), `rouge` (in-house token F1, no new dependencies), `llm_as_judge` (lazy: caller-supplied `judge_fn`), and `decision_scores` (composite over `semantica.context.Decision`)
- `decision_scores` validates field-level (expected outcome, confidence bounds, non-empty maker/reasoning/scenario) and governance-level (provenance record presence; opt-in `PolicyEngine.check_compliance`) checks, coercing dict inputs via `Decision(**actual)` and never crashing on malformed input; an interface slot for causal-chain/embedding checks is reserved and raises `NotImplementedError` (V2)
- `__version__` is `0.1.0`, and the module ships a usage guide at `semantica/evals/usage.md` with worked import/run/interpret examples
- `semantica.evals` is reachable through the root package lazy module proxy (`semantica.evals`)
- 99 unit tests in `tests/evals/` covering every evaluator, registry errors, runner aggregation, decision coercion, and per-metric objectives; `python -m pytest tests/evals -q` → 99 passed
- **First-class CrewAI integration** (#988, closes #962) by @Shindevrp
- New `pip install semantica[crewai]` extra (`crewai>=0.80.0`) — crewai core provides `BaseTool`/`BaseKnowledgeSource`, so `crewai-tools` is intentionally not included, and the extra is intentionally **not** part of the `all` bundle: crewai hard-requires `chromadb~=1.1.0`, which is affected by the unpatched pre-auth code-injection CVE-2026-45829 (see `integrations/crewai/README.md`)
+13 -16
View File
@@ -20,7 +20,7 @@
**Context Management &nbsp;·&nbsp; Knowledge Modeling &nbsp;·&nbsp; Deterministic Reasoning &nbsp;·&nbsp; Ontology Management &nbsp;·&nbsp; Decision Intelligence &nbsp;·&nbsp; End-to-End Traceability**
**Open Source &nbsp;·&nbsp; Self-Hostable &nbsp;·&nbsp; Auditable &nbsp;·&nbsp; Governed &nbsp;·&nbsp; Zero Vendor Lock-In**
**Open Source &nbsp;·&nbsp; Governed &nbsp;·&nbsp; Zero Vendor Lock-In**
**Polyglot Graph Storage &nbsp;·&nbsp; RDF & LPG Support &nbsp;·&nbsp; W3C Standards &nbsp;·&nbsp; Interoperable**
@@ -62,12 +62,12 @@ Most AI agents run on embeddings, not meaning: similarity scores with no structu
**Who it's for:**
- **AI/ML platform teams** shipping agents that make consequential decisions and need structured, queryable context built from fragmented raw data, not just a vector index
- **Data platform teams on Databricks or Snowflake** who need to turn tables already sitting in Unity Catalog or a Snowflake warehouse into a governed, lineage-tracked knowledge graph, without exporting that data to a third-party SaaS first
- **Compliance, risk, and audit teams** who need a straight answer to "why did the AI do that?" in a format a regulator will actually accept
- **Regulated enterprises** (finance, healthcare, legal, government, defense) that can't ship a black box, and can't send their data to someone else's SaaS to get one
- **AI/ML platform teams** shipping agents that make consequential decisions and need structured, queryable context, not just a vector index
- **Data platform teams on Databricks or Snowflake** turning tables already in Unity Catalog or a warehouse into a governed, lineage-tracked knowledge graph, without exporting to a third-party SaaS
- **Compliance, risk, and audit teams** who need a straight answer to "why did the AI do that?" in a format a regulator accepts
- **Regulated enterprises** (finance, healthcare, legal, government, defense) that can't ship a black box or send their data to someone else's SaaS to get one
- **Platform and infra engineers** who want the KG, reasoning, and provenance stack self-hosted and swappable, not locked to one vendor's backend
- **Data and knowledge engineers** building a KG from messy, multi-source data: entities and relationships get extracted, conflicting or contradictory facts are flagged instead of silently overwritten, and duplicates are merged before they turn into noise
- **Data and knowledge engineers** building a KG from messy, multi-source data, where conflicting facts get flagged and duplicates get merged, not silently overwritten
**[Quick Start](#quick-start)** &nbsp;·&nbsp; **[Architecture](#architecture)** &nbsp;·&nbsp; **[What You Get](#what-semantica-gives-you)** &nbsp;·&nbsp; **[Why Semantica](#why-semantica)** &nbsp;·&nbsp; **[Decision Intelligence](#decision-intelligence)** &nbsp;·&nbsp; **[Context Graphs](#context-graphs)** &nbsp;·&nbsp; **[Recipe: Audit Trail](#recipe-audit-trail-for-a-regulated-decision)** &nbsp;·&nbsp; **[Module Reference](#module-reference)** &nbsp;·&nbsp; **[Integrations](#integrations)** &nbsp;·&nbsp; **[CLI](#cli)** &nbsp;·&nbsp; **[Performance](#performance)** &nbsp;·&nbsp; **[Install](#installation)**
@@ -81,7 +81,7 @@ Most AI agents run on embeddings, not meaning: similarity scores with no structu
- **Full Auditability:** W3C PROV-O provenance on every fact, with audit trails exportable to JSON, CSV, or RDF
- **Deterministic Reasoning:** Forward chaining, Rete network, Datalog, and SPARQL with fully explainable paths, not black boxes
- **Knowledge Pipeline:** Multi-source ingestion, entity-aware chunking, NER/relation/event extraction, and knowledge graph construction, with semantic deduplication and provenance-preserving merges throughout
- **Enterprise Data Platforms:** Native connectors for Databricks (Unity Catalog + Delta Lake, PAT/OAuth M2M auth, catalog/schema/table/lineage introspection) and Snowflake (warehouse/database/schema, key-pair and OAuth auth), so tables already living in your lakehouse or warehouse become graph nodes with provenance, not another export/import hop
- **Enterprise Data Platforms:** Native connectors for Databricks (Unity Catalog + Delta Lake, PAT/OAuth M2M auth, catalog/schema/table/lineage introspection), Snowflake (warehouse/database/schema, key-pair and OAuth auth), and SAP OData (Business Partners, Sales Orders, OAuth2/Basic auth), so data already living in your lakehouse or warehouse becomes graph nodes with provenance, not another export/import hop
- **Graph Analytics:** Centrality, community detection, link prediction, and shortest-path queries over the graph you just built
- **Polyglot Graph Storage:** Native RDF (embedded Oxigraph, Blazegraph, Apache Jena, Eclipse RDF4J via SPARQL) and Labeled Property Graphs (Neo4j, FalkorDB, Apache AGE, AWS Neptune via Cypher), plus vector stores, all swappable without touching your code
- **Visualization:** Explore any graph, ontology, or timeline in an interactive browser workbench
@@ -139,10 +139,6 @@ compliant = graph.check_decision_rules({"category": "vendor_selection"}) # poli
```bash
semantica doctor
# Python 3.11.9 pass
# semantica 0.6.7 pass
# faiss vector store pass
# Config file pass ~/.semantica/config.yaml
```
**Running in a script or CI?** Progress bars are written only when stdout is an interactive terminal (or a Jupyter notebook), so piping and redirecting stay clean by default. Override with `SEMANTICA_DISABLE_PROGRESS=1` to silence progress everywhere, or `SEMANTICA_FORCE_PROGRESS=1` to keep it when stdout is redirected. `SEMANTICA_DISABLE_PROGRESS` takes precedence.
@@ -167,7 +163,7 @@ Sources → Ingest → Parse → Normalize → Split → Extract → Conflict De
→ Vector Store + Polyglot Graph Store (RDF & LPG) → Export / Visualize / REST · MCP · CLI
```
- **Ingest:** files, web, databases, enterprise data platforms (Databricks, Snowflake), cloud (Google Drive, Elasticsearch), streams (Kafka, Kinesis), Git, email, MCP
- **Ingest:** files, web, databases, enterprise data platforms (Databricks, Snowflake, SAP), cloud (Google Drive, Elasticsearch), streams (Kafka, Kinesis), Git, email, MCP
- **Parse → Normalize → Split:** document parsing, text/entity/date normalization, GraphRAG-native entity-aware chunking
- **Extract → Conflict Detection → Deduplication:** NER, relations, events, triplets; conflicting facts flagged and resolved before they merge
- **Knowledge Graph:** `GraphBuilder` constructs the graph; bi-temporal facts and full graph analytics (centrality, communities, link prediction) run on top of it
@@ -320,7 +316,7 @@ Every module below is independently importable, with working code samples verifi
| Module | What it does |
| --- | --- |
| [`semantica.ingest`](#semanticaingest-multi-source-ingestion) | Files, web, databases, APIs, streams, email, Git, Parquet, Databricks, Snowflake, MCP |
| [`semantica.ingest`](#semanticaingest-multi-source-ingestion) | Files, web, databases, APIs, streams, email, Git, Parquet, Databricks, Snowflake, SAP, MCP |
| [`semantica.semantic_extract`](#semanticasemantic_extract-ner-relations-events-triplets) | NER, relation extraction, event detection, triplet generation |
| [`semantica.kg`](#semanticakg-knowledge-graph-construction--analysis) | Graph construction, centrality, communities, link prediction |
| [`semantica.reasoning`](#semanticareasoning-forward-chaining-rete-datalog-sparql) | Forward chaining, Rete, Datalog, SPARQL, fully explainable |
@@ -349,7 +345,7 @@ Expand any module below for its runnable example.
<summary><b><code>semantica.ingest</code></b>: Multi-Source Ingestion</summary>
<a id="semanticaingest-multi-source-ingestion"></a>
Ingest from files, web, databases, APIs, streams, email, Git repos, Parquet, Databricks, Snowflake, or MCP servers, all through a unified interface.
Ingest from files, web, databases, APIs, streams, email, Git repos, Parquet, Databricks, Snowflake, SAP, or MCP servers, all through a unified interface.
```python
from semantica.ingest import FileIngestor, WebIngestor, ParquetIngestor, DBIngestor
@@ -400,7 +396,7 @@ orders = snowflake.ingest_table("ORDERS", limit=10_000)
> **Security Note:** Never hardcode credentials (`token`, `password`, `private_key`) in production code; pass them via environment variables (e.g., `DATABRICKS_TOKEN`, `SNOWFLAKE_PASSWORD`) or a secrets manager.
**Supported sources:** Local files (PDF, DOCX, PPTX, HTML, TXT, CSV, JSON, YAML, Excel, XML) · Web pages · RSS/Atom feeds · REST APIs · Databases (PostgreSQL, MySQL, SQLite, Oracle, SQL Server) · Parquet datasets · Databricks (Unity Catalog + Delta Lake) · Snowflake · Git repositories · Email (IMAP/POP3) · Message streams (Kafka, RabbitMQ, Kinesis, Pulsar) · MCP resources · Apache Arrow/Feather/IPC (`ArrowIngestor`)
**Supported sources:** Local files (PDF, DOCX, PPTX, HTML, TXT, CSV, JSON, YAML, Excel, XML) · Web pages · RSS/Atom feeds · REST APIs · Databases (PostgreSQL, MySQL, SQLite, Oracle, SQL Server) · Parquet datasets · Databricks (Unity Catalog + Delta Lake) · Snowflake · SAP (OData v2/v4) · Git repositories · Email (IMAP/POP3) · Message streams (Kafka, RabbitMQ, Kinesis, Pulsar) · MCP resources · Apache Arrow/Feather/IPC (`ArrowIngestor`)
DuckDB, Elasticsearch, Google Drive, HuggingFace, MongoDB, and Pandas ingestion also ship (`DuckDBIngestor`, `ElasticIngestor`, `GDriveIngestor`, `HuggingFaceIngestor`, `MongoIngestor`, `PandasIngestor`) but aren't re-exported from the top-level `semantica.ingest` namespace yet — import them directly: `from semantica.ingest.duckdb_ingestor import DuckDBIngestor`.
@@ -1145,7 +1141,7 @@ if report.valid:
| **Vector Store** | FAISS · Pinecone · Weaviate · Qdrant · Milvus · PgVector · hybrid + filtered search |
| **Graph Databases (LPG)** | Neo4j · FalkorDB · Apache AGE · AWS Neptune |
| **Triple Stores (RDF)** | Oxigraph (embedded) · Blazegraph · Apache Jena · Eclipse RDF4J · unified `TripletStore` interface · SPARQL query & bulk load |
| **Enterprise Data Platforms** | Databricks (`DatabricksIngestor`: Unity Catalog + Delta Lake, PAT/OAuth M2M, table/query ingestion, catalog/schema/table/lineage introspection) · Snowflake (`SnowflakeIngestor`: warehouse/database/schema, password/key-pair/OAuth auth) |
| **Enterprise Data Platforms** | Databricks (`DatabricksIngestor`: Unity Catalog + Delta Lake, PAT/OAuth M2M, table/query ingestion, catalog/schema/table/lineage introspection) · Snowflake (`SnowflakeIngestor`: warehouse/database/schema, password/key-pair/OAuth auth) · SAP (`SAPIngestor`: OData v2/v4, OAuth2/Basic auth, Business Partners/Sales Orders) |
| **LLM Providers** | **All already supported today:** OpenAI (GPT-4o, o1, o3) · Anthropic (Claude) · Google Gemini · Mistral · Meta Llama · Groq · Cohere · Azure OpenAI · AWS Bedrock · Ollama · DeepSeek · Perplexity · Together AI · Fireworks AI · Replicate · HuggingFace · via `semantica.llms` and LiteLLM |
---
@@ -1517,6 +1513,7 @@ pip install semantica[vectorstore-qdrant] # Qdrant vector store
pip install semantica[vectorstore-pinecone] # Pinecone vector store
pip install semantica[db-snowflake] # Snowflake
pip install semantica[db-databricks] # Databricks (SDK + SQL connector)
pip install semantica[ingest-sap] # SAP OData
pip install semantica[ingest-parquet] # Parquet / PyArrow
pip install semantica[ingest-arrow] # Apache Arrow, Feather, IPC
pip install semantica[viz] # HTML interactive visualization
+2 -3
View File
@@ -153,7 +153,7 @@ that attack chain.
- **Risk**: a PR merges without its security/CI checks passing.
**Control**: merges require the `build`, `Analyze Python` (CodeQL), and `security-scan` checks to pass, in strict mode (checks must be re-run against the latest `main`).
- **Risk**: a compromised scanner job reaches secrets or write access.
**Control**: scanning jobs (`CodeQL`, `security-scan.yml`, `security.yml`, `defender-for-devops.yml`) run with read-only, least-privilege permissions (typically `contents: read` + `security-events: write` only) and never share a job, environment, or secret scope with the publish job.
**Control**: scanning jobs (`CodeQL`, `security-scan.yml`, `defender-for-devops.yml`) run with read-only, least-privilege permissions (typically `contents: read` + `security-events: write` only) and never share a job, environment, or secret scope with the publish job.
- **Risk**: secrets are committed accidentally.
**Control**: GitHub secret scanning and push protection are both enabled at the repository level, rejecting pushes that contain recognizable credential patterns before they land in history.
@@ -164,8 +164,7 @@ Every scan below runs continuously in CI, not just at release time:
- **CodeQL** (`security-and-quality` query pack) — Python source: injection, unsafe deserialization, and other code-level vulnerability classes. Runs in `codeql.yml` on every push/PR to `main` and weekly.
- **Bandit** — Python-specific security anti-patterns (hardcoded secrets, unsafe `eval`/`pickle`, weak crypto, etc.); CI fails on any HIGH-severity finding. Runs in `security-scan.yml` on every push/PR to `main` and twice weekly.
- **Semgrep** (`p/security` ruleset) — cross-language static-analysis security patterns. Runs in `security-scan.yml` on every push/PR to `main` and twice weekly.
- **Safety** — known CVEs in Semantica's own installed dependencies, including optional LLM-provider extras such as LiteLLM; CI fails on any match. Runs in `security-scan.yml` on every push/PR to `main` and twice weekly.
- **pip-audit** — independent, PyPA-maintained vulnerability database cross-check against installed dependencies (Safety and pip-audit use different advisory sources, so both run). Runs in `security.yml` weekly.
- **pip-audit** — PyPA-maintained, OSV-backed vulnerability database cross-check against Semantica's pinned dependency tree, including optional LLM-provider extras such as LiteLLM; CI fails on any match. Runs in `security-scan.yml` on every push/PR to `main` and twice weekly, and can be triggered on demand via `workflow_dispatch`.
- **Microsoft Defender for DevOps** (`eslint`, `templateanalyzer`, `terrascan`) — JavaScript/TypeScript lint-security rules and infrastructure-as-code misconfigurations. Runs in `defender-for-devops.yml` on every push/PR to `main` and weekly.
- **Checkov** — Kubernetes, Helm, Dockerfile, GitHub Actions, and secrets-pattern IaC scanning; results upload to the same Security tab as CodeQL. Runs in `defender-for-devops.yml` on every push/PR to `main` and weekly.
- **GitGuardian** — secret-detection check on every pull request, installed as a GitHub App integration (not a repo-local workflow). Runs on every PR.
@@ -1,222 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/semantica-agi/semantica/blob/main/cookbook/advanced/09_Semantic_Layer_Construction.ipynb)\n",
"\n",
"# Semantic Layer Construction\n",
"\n",
"## Overview\n",
"\n",
"Build an enterprise semantic layer: construct knowledge graph, generate ontology, create semantic layer, export RDF, and store in triplet store.\n",
"\n",
"\n",
"**Documentation**: [API Reference](https://semantica.readthedocs.io/concepts/)\n",
"\n",
"## Installation\n",
"\n",
"Install Semantica from PyPI:\n",
"\n",
"```bash\n",
"pip install semantica\n",
"# Or with all optional dependencies:\n",
"pip install semantica[all]\n",
"```\n",
"\n",
"## Workflow: Build KG → Generate Ontology → Create Semantic Layer → Export RDF \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!pip install -qU semantica\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from semantica.kg import GraphBuilder\n",
"from semantica.ontology import OntologyGenerator\n",
"from semantica.export import RDFExporter\n",
"from semantica.triplet_store import TripletStore\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 1: Build Knowledge Graph\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"builder = GraphBuilder()\n",
"\n",
"entities = [\n",
" {\"id\": \"e1\", \"type\": \"Person\", \"name\": \"Alice\", \"properties\": {\"age\": 30, \"role\": \"Engineer\"}},\n",
" {\"id\": \"e2\", \"type\": \"Person\", \"name\": \"Bob\", \"properties\": {\"age\": 35, \"role\": \"Manager\"}},\n",
" {\"id\": \"e3\", \"type\": \"Organization\", \"name\": \"Tech Corp\", \"properties\": {\"founded\": 2010}},\n",
" {\"id\": \"e4\", \"type\": \"Project\", \"name\": \"Project Alpha\", \"properties\": {\"status\": \"active\"}},\n",
"]\n",
"\n",
"relationships = [\n",
" {\"source\": \"e1\", \"target\": \"e2\", \"type\": \"reports_to\"},\n",
" {\"source\": \"e1\", \"target\": \"e3\", \"type\": \"works_for\"},\n",
" {\"source\": \"e2\", \"target\": \"e3\", \"type\": \"works_for\"},\n",
" {\"source\": \"e1\", \"target\": \"e4\", \"type\": \"works_on\"},\n",
"]\n",
"\n",
"knowledge_graph = builder.build(entities, relationships)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Generate Ontology\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"generator = OntologyGenerator()\n",
"ontology = generator.generate_from_graph(knowledge_graph)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Create Semantic Layer\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def create_mappings(kg, ontology):\n",
" mappings = {\n",
" \"entity_type_mappings\": {},\n",
" \"relationship_type_mappings\": {},\n",
" \"property_mappings\": {}\n",
" }\n",
" \n",
" entity_types = set(e.get(\"type\") for e in entities)\n",
" ontology_classes = ontology.get(\"classes\", [])\n",
" \n",
" for entity_type in entity_types:\n",
" matching_class = next((cls for cls in ontology_classes if cls.get(\"name\") == entity_type), None)\n",
" if matching_class:\n",
" mappings[\"entity_type_mappings\"][entity_type] = matching_class.get(\"uri\", entity_type)\n",
" \n",
" relationship_types = set(r.get(\"type\") for r in relationships)\n",
" ontology_properties = ontology.get(\"properties\", [])\n",
" \n",
" for rel_type in relationship_types:\n",
" matching_prop = next((prop for prop in ontology_properties if prop.get(\"name\") == rel_type), None)\n",
" if matching_prop:\n",
" mappings[\"relationship_type_mappings\"][rel_type] = matching_prop.get(\"uri\", rel_type)\n",
" \n",
" return mappings\n",
"\n",
"mappings = create_mappings(knowledge_graph, ontology)\n",
"\n",
"semantic_layer = {\n",
" \"graph\": knowledge_graph,\n",
" \"ontology\": ontology,\n",
" \"mappings\": mappings,\n",
" \"metadata\": {\n",
" \"version\": \"1.0\",\n",
" \"created_at\": \"2024-01-01\",\n",
" \"description\": \"Enterprise semantic layer\"\n",
" }\n",
"}\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Export RDF\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"exporter = RDFExporter()\n",
"# Export Knowledge Graph\n",
"exporter.export(knowledge_graph, \"knowledge_graph.ttl\", format=\"turtle\")\n",
"print(\"Exported knowledge graph to knowledge_graph.ttl\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"Enterprise semantic layer construction:\n",
"- Knowledge Graph Built\n",
"- Ontology Generated\n",
"- Semantic Layer Created with Mappings\n",
"- RDF Export Completed\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -1,435 +0,0 @@
{
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"cells": [
{
"cell_type": "markdown",
"id": "cell-0",
"metadata": {},
"source": [
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/semantica-agi/semantica/blob/main/cookbook/advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb)\n",
"\n",
"# Manual Ontology + Snowflake Mapping\n",
"\n",
"This notebook answers a specific workflow:\n",
"\n",
"> *\"I want to design the ontology myself — not have AI infer it from my tables — and then map Snowflake data to it explicitly.\"*\n",
"\n",
"### What this notebook demonstrates\n",
"\n",
"| Step | What happens | Who controls it |\n",
"|---|---|---|\n",
"| 1 | Design ontology classes and properties | **You** (Python dict) |\n",
"| 2 | Model n-ary facts with reification | **You** (`AssociativeClassBuilder`) |\n",
"| 3 | Pull rows from Snowflake | Semantica `SnowflakeIngestor` |\n",
"| 4 | Map columns → ontology-aligned graph | **You** (explicit transform) |\n",
"| 5 | Validate + export OWL / SHACL | Semantica `OntologyEngine` |\n",
"| 6 | Load to triplet store and query | Semantica `TripletStore` |\n",
"\n",
"### What this notebook does NOT do\n",
"\n",
"- No LLM-driven ontology generation\n",
"- No schema introspection or table-to-class inference\n",
"- No \"suggest ontology from my data\"\n",
"\n",
"### Standards coverage\n",
"\n",
"| Feature | Status |\n",
"|---|---|\n",
"| OWL 2 (Turtle / RDF-XML) | Supported |\n",
"| SHACL 1.1 shapes | Supported |\n",
"| SPARQL 1.1 | Supported |\n",
"| Reification / n-ary facts | Supported via `AssociativeClassBuilder` |\n",
"| SPARQL 1.2 (reifier annotation, `LATERAL`) | Planned |\n",
"| SHACL 1.2 (`sh:severity` extensions, SHACL-AF) | Planned |"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-1",
"metadata": {},
"outputs": [],
"source": [
"!pip install -qU semantica"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-2",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from typing import Any, Dict, List\n",
"\n",
"from semantica.ingest import SnowflakeIngestor\n",
"from semantica.kg.methods import build_kg\n",
"from semantica.ontology import AssociativeClassBuilder, OntologyEngine\n",
"from semantica.triplet_store import TripletStore"
]
},
{
"cell_type": "markdown",
"id": "cell-3",
"metadata": {},
"source": [
"## Step 1: Hand-Design the Ontology in Python\n",
"\n",
"You define every class and property explicitly. Nothing is read from Snowflake at this stage.\n",
"\n",
"**Design decisions that belong to you:**\n",
"- Which classes exist and what they mean\n",
"- Which properties are datatype vs. object properties\n",
"- Domain, range, and cardinality constraints\n",
"- Which properties are required (later enforced by SHACL)\n",
"\n",
"This dict versions with your code. It does not change when your database schema changes."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-4",
"metadata": {},
"outputs": [],
"source": "BASE_URI = \"https://example.com/hr/\"\n\n# Your ontology — designed by you, not inferred by Semantica.\nontology: Dict[str, Any] = {\n \"name\": \"EmploymentDomainOntology\",\n \"uri\": f\"{BASE_URI}EmploymentDomainOntology\",\n \"namespace\": {\"base_uri\": BASE_URI},\n\n # You decide the class taxonomy\n \"classes\": [\n {\"name\": \"Person\", \"uri\": f\"{BASE_URI}Person\"},\n {\"name\": \"Organization\", \"uri\": f\"{BASE_URI}Organization\"},\n {\"name\": \"Role\", \"uri\": f\"{BASE_URI}Role\"},\n # EmploymentEvent is a reification node.\n # It connects Person + Organization + Role and carries salary/date context.\n {\"name\": \"EmploymentEvent\", \"uri\": f\"{BASE_URI}EmploymentEvent\"},\n ],\n\n # Each property carries a full URI so TripletStore stores it as hr:<name>\n # rather than the default urn:property:<name>.\n # This ensures SPARQL queries using PREFIX hr: match what is actually stored.\n \"properties\": [\n # Datatype properties\n {\"name\": \"name\", \"uri\": f\"{BASE_URI}name\", \"type\": \"datatype\", \"domain\": \"Person\", \"range\": \"string\", \"required\": True},\n {\"name\": \"legalName\", \"uri\": f\"{BASE_URI}legalName\", \"type\": \"datatype\", \"domain\": \"Organization\", \"range\": \"string\", \"required\": True},\n {\"name\": \"title\", \"uri\": f\"{BASE_URI}title\", \"type\": \"datatype\", \"domain\": \"Role\", \"range\": \"string\", \"required\": True},\n {\"name\": \"startDate\", \"uri\": f\"{BASE_URI}startDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n {\"name\": \"endDate\", \"uri\": f\"{BASE_URI}endDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n {\"name\": \"salary\", \"uri\": f\"{BASE_URI}salary\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"decimal\"},\n\n # Object properties — reification spokes (required)\n {\"name\": \"employee\", \"uri\": f\"{BASE_URI}employee\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Person\", \"required\": True},\n {\"name\": \"employer\", \"uri\": f\"{BASE_URI}employer\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Organization\", \"required\": True},\n {\"name\": \"role\", \"uri\": f\"{BASE_URI}role\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Role\", \"required\": True},\n\n # Shortcut edges — direct person→org / person→role without traversing the event node\n {\"name\": \"worksFor\", \"uri\": f\"{BASE_URI}worksFor\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Organization\"},\n {\"name\": \"hasRole\", \"uri\": f\"{BASE_URI}hasRole\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Role\"},\n ],\n}\n\nontology"
},
{
"cell_type": "markdown",
"id": "cell-5",
"metadata": {},
"source": [
"## Step 2: Reification — Modeling N-Ary Facts\n",
"\n",
"**The problem with binary triples:**\n",
"A simple triple `(Alice, worksFor, Acme)` cannot carry extra context such as salary, start date, or role.\n",
"Standard RDF reification and OWL n-ary patterns solve this by introducing an intermediate node.\n",
"\n",
"Semantica's `AssociativeClassBuilder` is the Pythonic API for this pattern:\n",
"\n",
"```\n",
"EmploymentEvent\n",
" ├── employee → Person (required)\n",
" ├── employer → Organization (required)\n",
" ├── role → Role (required)\n",
" ├── startDate → xsd:date\n",
" ├── endDate → xsd:date\n",
" └── salary → xsd:decimal\n",
"```\n",
"\n",
"**On SPARQL 1.1 vs. SPARQL 1.2:**\n",
"- **SPARQL 1.1 (current):** traverse the event node explicitly — `?event hr:employee ?person ; hr:salary ?salary`\n",
"- **SPARQL 1.2 (planned):** the draft reifier annotation syntax allows attaching context to triples directly, without a separate intermediate node. Semantica will adopt this once the spec is ratified.\n",
"\n",
"**On SHACL 1.1 vs. SHACL 1.2:**\n",
"- **SHACL 1.1 (current):** `sh:NodeShape` + `sh:PropertyShape` constraints are exported for all `required` properties and enforced at load time.\n",
"- **SHACL 1.2 (planned):** `sh:severity` profile extensions and SHACL-AF rules are on the roadmap."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-6",
"metadata": {},
"outputs": [],
"source": "assoc_builder = AssociativeClassBuilder()\n\nemployment_assoc = assoc_builder.create_associative_class(\n name=\"EmploymentEvent\",\n connects=[\"Person\", \"Organization\", \"Role\"],\n temporal=True, # adds startDate / endDate handling\n properties={\n \"startDate\": \"xsd:date\",\n \"endDate\": \"xsd:date\",\n \"salary\": \"xsd:decimal\",\n },\n)\n\nvalidation_result = assoc_builder.validate_associative_class(employment_assoc)\n\n# AssociativeClass is a dataclass — use attribute access, not .get()\nprint(\"AssociativeClass structure:\")\nprint(f\" name: {employment_assoc.name}\")\nprint(f\" connects: {employment_assoc.connects}\")\nprint(f\" temporal: {employment_assoc.temporal}\")\nprint(f\" properties: {list(employment_assoc.properties.keys())}\")\nprint(f\"\\nValidation passed: {validation_result}\")"
},
{
"cell_type": "markdown",
"id": "cell-7",
"metadata": {},
"source": [
"## Step 3: Ingest Snowflake Rows (Extraction Only)\n",
"\n",
"`SnowflakeIngestor` retrieves rows — nothing more. It does **not**:\n",
"- Inspect your table schema\n",
"- Suggest classes or properties\n",
"- Infer relationships from column names\n",
"\n",
"Set `USE_LIVE_SNOWFLAKE=true` plus the env vars below to connect to a real warehouse.\n",
"Otherwise the stub data is used."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-8",
"metadata": {},
"outputs": [],
"source": [
"def fetch_rows_from_snowflake() -> List[Dict[str, Any]]:\n",
" if os.getenv(\"USE_LIVE_SNOWFLAKE\", \"false\").lower() != \"true\":\n",
" return [\n",
" {\n",
" \"EMPLOYEE_ID\": \"E100\",\n",
" \"EMPLOYEE_NAME\": \"Alice Johnson\",\n",
" \"ORG_ID\": \"O10\",\n",
" \"ORG_NAME\": \"Acme Corp\",\n",
" \"ROLE_ID\": \"R7\",\n",
" \"ROLE_TITLE\": \"Senior Engineer\",\n",
" \"START_DATE\": \"2025-01-15\",\n",
" \"END_DATE\": None,\n",
" \"SALARY\": 160000,\n",
" },\n",
" {\n",
" \"EMPLOYEE_ID\": \"E101\",\n",
" \"EMPLOYEE_NAME\": \"Bob Singh\",\n",
" \"ORG_ID\": \"O10\",\n",
" \"ORG_NAME\": \"Acme Corp\",\n",
" \"ROLE_ID\": \"R9\",\n",
" \"ROLE_TITLE\": \"Data Architect\",\n",
" \"START_DATE\": \"2024-09-01\",\n",
" \"END_DATE\": None,\n",
" \"SALARY\": 185000,\n",
" },\n",
" ]\n",
"\n",
" ingestor = SnowflakeIngestor(\n",
" account=os.getenv(\"SNOWFLAKE_ACCOUNT\"),\n",
" user=os.getenv(\"SNOWFLAKE_USER\"),\n",
" password=os.getenv(\"SNOWFLAKE_PASSWORD\"),\n",
" warehouse=os.getenv(\"SNOWFLAKE_WAREHOUSE\"),\n",
" database=os.getenv(\"SNOWFLAKE_DATABASE\"),\n",
" schema=os.getenv(\"SNOWFLAKE_SCHEMA\", \"PUBLIC\"),\n",
" )\n",
" query = (\n",
" \"SELECT EMPLOYEE_ID, EMPLOYEE_NAME, \"\n",
" \"ORG_ID, ORG_NAME, ROLE_ID, ROLE_TITLE, \"\n",
" \"START_DATE, END_DATE, SALARY \"\n",
" \"FROM HR_EMPLOYMENT_FACT\"\n",
" )\n",
" data = ingestor.ingest_query(query)\n",
" ingestor.close()\n",
" return data.data\n",
"\n",
"\n",
"rows = fetch_rows_from_snowflake()\n",
"rows[:2]"
]
},
{
"cell_type": "markdown",
"id": "cell-9",
"metadata": {},
"source": [
"## Step 4: Map Rows to Ontology Concepts Explicitly\n",
"\n",
"This is the semantic transformation layer — the part that makes your ontology real.\n",
"\n",
"Semantica does not guess which column becomes which entity or property.\n",
"Every assignment is code you write and own:\n",
"\n",
"- **Stable node IDs** — deterministic, collision-safe, derived from business keys\n",
"- **Class assignment** — matches what you declared in Step 1\n",
"- **Property routing** — each column value goes to the correct ontology property\n",
"- **Reification wiring** — `EmploymentEvent` is linked to its three participants\n",
"\n",
"When your Snowflake schema changes, only this function needs updating. The ontology stays stable."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-10",
"metadata": {},
"outputs": [],
"source": "def map_rows_to_kg(rows: List[Dict[str, Any]]) -> Dict[str, Any]:\n entities: Dict[str, Dict[str, Any]] = {}\n relationships: List[Dict[str, Any]] = []\n\n for row in rows:\n # Stable, deterministic node IDs derived from business keys\n person_id = f\"person:{row['EMPLOYEE_ID']}\"\n org_id = f\"org:{row['ORG_ID']}\"\n role_id = f\"role:{row['ROLE_ID']}\"\n # Event ID includes all three participants + start date so that\n # a re-hired employee gets a distinct event node, not an overwrite.\n event_id = f\"employment:{row['EMPLOYEE_ID']}:{row['ORG_ID']}:{row['START_DATE']}\"\n\n # Entities — \"type\" must match a class name from Step 1\n entities[person_id] = {\n \"id\": person_id,\n \"type\": \"Person\",\n \"properties\": {\"name\": row[\"EMPLOYEE_NAME\"]},\n }\n entities[org_id] = {\n \"id\": org_id,\n \"type\": \"Organization\",\n \"properties\": {\"legalName\": row[\"ORG_NAME\"]},\n }\n entities[role_id] = {\n \"id\": role_id,\n \"type\": \"Role\",\n \"properties\": {\"title\": row[\"ROLE_TITLE\"]},\n }\n\n # Reification node — filter out None values so TripletStore does not\n # stringify None as the literal \"None\" for open-ended employment.\n event_props = {\n \"startDate\": row[\"START_DATE\"],\n \"endDate\": row[\"END_DATE\"],\n \"salary\": row[\"SALARY\"],\n }\n entities[event_id] = {\n \"id\": event_id,\n \"type\": \"EmploymentEvent\",\n \"properties\": {k: v for k, v in event_props.items() if v is not None},\n }\n\n # Full URIs for relationship types so TripletStore stores hr:<type>\n # instead of the default urn:property:<type>, keeping SPARQL consistent.\n relationships.extend([\n # Shortcut edges — fast SPARQL when context is not needed\n {\"source\": person_id, \"target\": org_id, \"type\": f\"{BASE_URI}worksFor\"},\n {\"source\": person_id, \"target\": role_id, \"type\": f\"{BASE_URI}hasRole\"},\n # Reification spokes — full context via the event node\n {\"source\": event_id, \"target\": person_id, \"type\": f\"{BASE_URI}employee\"},\n {\"source\": event_id, \"target\": org_id, \"type\": f\"{BASE_URI}employer\"},\n {\"source\": event_id, \"target\": role_id, \"type\": f\"{BASE_URI}role\"},\n ])\n\n return build_kg([{\"entities\": list(entities.values()), \"relationships\": relationships}])\n\n\nkg = map_rows_to_kg(rows)\nprint(f\"Entities built: {len(kg.get('entities', []))}\")\nprint(f\"Relationships built: {len(kg.get('relationships', []))}\")\n\nsample = next((e for e in kg[\"entities\"] if e[\"type\"] == \"EmploymentEvent\"), None)\nprint(f\"\\nSample EmploymentEvent node: {sample}\")"
},
{
"cell_type": "markdown",
"id": "cell-11",
"metadata": {},
"source": [
"## Step 5: Validate Ontology and Export OWL + SHACL\n",
"\n",
"`OntologyEngine` validates your ontology dict and serialises it to standards-compliant files.\n",
"\n",
"**Output files:**\n",
"- `employment_manual_ontology.ttl` — OWL 2 Turtle\n",
"- `employment_manual_shapes.ttl` — SHACL 1.1 node and property shapes\n",
"\n",
"**Standards status:**\n",
"\n",
"| Standard | Semantica support |\n",
"|---|---|\n",
"| SPARQL 1.1 | Full |\n",
"| SHACL 1.1 (`sh:NodeShape`, `sh:PropertyShape`, `sh:minCount`, `sh:datatype`, `sh:class`) | Full |\n",
"| SPARQL 1.2 (reifier annotation syntax, `LATERAL`) | Tracked — not yet implemented |\n",
"| SHACL 1.2 (`sh:severity` profiles, SHACL-AF extensions) | Tracked — not yet implemented |"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-12",
"metadata": {},
"outputs": [],
"source": [
"engine = OntologyEngine(base_uri=BASE_URI)\n",
"\n",
"validation = engine.validate(ontology)\n",
"owl_ttl = engine.to_owl(ontology, format=\"turtle\")\n",
"shacl_ttl = engine.to_shacl(ontology, format=\"turtle\")\n",
"\n",
"engine.export_owl(ontology, \"employment_manual_ontology.ttl\", format=\"turtle\")\n",
"engine.export_shacl(ontology, \"employment_manual_shapes.ttl\", format=\"turtle\")\n",
"\n",
"print(f\"Ontology valid: {validation.valid}\")\n",
"print(f\"Ontology consistent: {validation.consistent}\")\n",
"print(f\"OWL output: {len(owl_ttl):,} chars → employment_manual_ontology.ttl\")\n",
"print(f\"SHACL output: {len(shacl_ttl):,} chars → employment_manual_shapes.ttl\")\n",
"\n",
"print(\"\\n--- SHACL shapes (first 20 lines) ---\")\n",
"print(\"\\n\".join(shacl_ttl.splitlines()[:20]))"
]
},
{
"cell_type": "markdown",
"id": "cell-13",
"metadata": {},
"source": [
"## Best-Practice Architecture\n",
"\n",
"```\n",
"┌──────────────────────────────────┐\n",
"│ Ontology as code (Python dict) │ ← versioned alongside your application\n",
"│ + AssociativeClass for n-ary │\n",
"└───────────────┬──────────────────┘\n",
" │ validate + export\n",
" ▼\n",
"┌───────────────────────────────────┐\n",
"│ OWL 2 Turtle │ SHACL 1.1 │ ← standards-compliant artifacts\n",
"└───────────────┬───────────────────┘\n",
" │\n",
" ▼\n",
"┌──────────────────────────────────┐\n",
"│ Snowflake — raw data access │ ← no schema introspection\n",
"└───────────────┬──────────────────┘\n",
" │ explicit mapping layer\n",
" ▼\n",
"┌──────────────────────────────────┐\n",
"│ Ontology-aligned KG │ ← types, IDs, edges match Step 1\n",
"└───────────────┬──────────────────┘\n",
" │ optional\n",
" ▼\n",
"┌──────────────────────────────────┐\n",
"│ Triplet store + SPARQL 1.1 │\n",
"└──────────────────────────────────┘\n",
"```\n",
"\n",
"**Why this split matters:**\n",
"If Semantica inferred the ontology from your Snowflake schema, every schema migration would risk silently changing your semantic model.\n",
"With this pattern, schema changes only touch the mapping function in Step 4 — the ontology remains stable and under your control."
]
},
{
"cell_type": "markdown",
"id": "cell-14",
"metadata": {},
"source": [
"## SPARQL Query Patterns\n",
"\n",
"Two query styles are available because we wrote both shortcut edges and reification spokes.\n",
"\n",
"### Simple lookup — shortcut edge (no context needed)\n",
"\n",
"```sparql\n",
"PREFIX hr: <https://example.com/hr/>\n",
"\n",
"SELECT ?personName ?orgName\n",
"WHERE {\n",
" ?person a hr:Person ;\n",
" hr:name ?personName ;\n",
" hr:worksFor ?org .\n",
" ?org hr:legalName ?orgName .\n",
"}\n",
"```\n",
"\n",
"### Contextual lookup — via reification node (salary, dates, role)\n",
"\n",
"```sparql\n",
"PREFIX hr: <https://example.com/hr/>\n",
"\n",
"SELECT ?personName ?roleTitle ?salary ?startDate\n",
"WHERE {\n",
" ?event a hr:EmploymentEvent ;\n",
" hr:employee ?person ;\n",
" hr:role ?role ;\n",
" hr:salary ?salary ;\n",
" hr:startDate ?startDate .\n",
" ?person hr:name ?personName .\n",
" ?role hr:title ?roleTitle .\n",
"}\n",
"ORDER BY DESC(?salary)\n",
"```\n",
"\n",
"### Future: SPARQL 1.2 reifier syntax\n",
"\n",
"The SPARQL 1.2 draft introduces annotation syntax that lets you attach context directly to triples, without a separate intermediate node.\n",
"Once the spec is ratified Semantica will adopt it, and the contextual query above may be expressible more concisely."
]
},
{
"cell_type": "markdown",
"id": "cell-15",
"metadata": {},
"source": [
"## Step 6 (Optional): Load to Triplet Store and Run SPARQL\n",
"\n",
"Set `STORE_TO_TRIPLET=true` to load the KG into a live triplet store and run the contextual reification query."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-16",
"metadata": {},
"outputs": [],
"source": [
"if os.getenv(\"STORE_TO_TRIPLET\", \"false\").lower() == \"true\":\n",
" store = TripletStore(\n",
" backend=os.getenv(\"TRIPLET_BACKEND\", \"blazegraph\"),\n",
" endpoint=os.getenv(\"TRIPLET_ENDPOINT\", \"http://localhost:9999/blazegraph\"),\n",
" namespace=os.getenv(\"TRIPLET_NAMESPACE\", \"kb\"),\n",
" )\n",
" store_result = store.store(knowledge_graph=kg, ontology=ontology)\n",
" print(\"Store result:\", store_result)\n",
"\n",
" # Contextual reification query — person + role + salary via EmploymentEvent\n",
" query = \"\"\"\n",
" PREFIX hr: <https://example.com/hr/>\n",
"\n",
" SELECT ?personName ?roleTitle ?salary ?startDate\n",
" WHERE {\n",
" ?event a hr:EmploymentEvent ;\n",
" hr:employee ?person ;\n",
" hr:role ?role ;\n",
" hr:salary ?salary ;\n",
" hr:startDate ?startDate .\n",
" ?person hr:name ?personName .\n",
" ?role hr:title ?roleTitle .\n",
" }\n",
" ORDER BY DESC(?salary)\n",
" LIMIT 10\n",
" \"\"\"\n",
" result = store.execute_query(query)\n",
" print(result)\n",
"else:\n",
" print(\"Skipping triplet-store load/query (set STORE_TO_TRIPLET=true to enable)\")"
]
}
]
}
@@ -10,7 +10,7 @@
"\n",
"## Overview\n",
"\n",
"This notebook walks you through creating your first knowledge graph from a simple document. You'll learn the complete end-to-end workflow from ingesting a file to visualizing the resulting knowledge graph.\n",
"This notebook walks you through creating your first knowledge graph from a simple document. You'll learn the complete end-to-end workflow from ingesting a file to visualizing the resulting knowledge graph — and every step consumes the real output of the step before it.\n",
"\n",
"> [!TIP]\n",
"> This is the perfect starting point if you are new to Semantica. No prior knowledge of knowledge graphs is required!\n",
@@ -19,10 +19,10 @@
"\n",
"### 🎯 Learning Objectives\n",
"\n",
"- **Understand the Workflow**: Learn the `File → Parse → Extract → Graph` pipeline\n",
"- **Understand the Workflow**: Learn the `File → Parse → Extract → Graph → Visualize` pipeline\n",
"- **Ingest Data**: Load documents using `FileIngestor`\n",
"- **Parse Content**: Extract text using `DocumentParser`\n",
"- **Extract Knowledge**: Identify entities using `NERExtractor`\n",
"- **Extract Knowledge**: Identify entities and relations using `NERExtractor` and `RelationExtractor`\n",
"- **Build Graph**: Construct a graph using `GraphBuilder`\n",
"- **Visualize**: See your graph come to life with `KGVisualizer`\n",
"\n",
@@ -40,71 +40,76 @@
"\n",
"## 🔄 Simple End-to-End Workflow\n",
"\n",
"The complete workflow consists of four main steps:\n",
"The complete workflow consists of five main steps:\n",
"\n",
"1. **📥 Ingest** - Load data from files or other sources\n",
"2. **📄 Parse** - Extract and structure content from documents\n",
"3. **⛏️ Extract** - Identify entities and relationships\n",
"4. **🕸️ Build Graph** - Construct the knowledge graph\n",
"5. **📊 Visualize** - Render and analyze the graph\n",
"\n",
"Each step is demonstrated in the code cells below.\n",
"Each step is demonstrated in the code cells below, and each cell can be rerun on its own: the sample file is only removed by the optional cleanup cell at the very end.\n",
"\n",
"> [!TIP]\n",
"> **Alternative: Using Semantica Framework**\n",
"> \n",
">\n",
"> For a simpler, high-level approach, you can use the `Semantica` framework class which orchestrates all these steps:\n",
"> \n",
">\n",
"> ```python\n",
"> from semantica.core import Semantica\n",
"> \n",
">\n",
"> framework = Semantica()\n",
"> framework.initialize()\n",
"> \n",
">\n",
"> result = framework.build_knowledge_base(\n",
"> sources=[\"sample_document.txt\"],\n",
"> embeddings=True,\n",
"> graph=True\n",
"> )\n",
"> \n",
">\n",
"> framework.shutdown()\n",
"> ```\n",
"> \n",
">\n",
"> This notebook shows the step-by-step approach for learning. See [Core Module Usage Guide](../../../semantica/core/core_usage.md) for more details.\n",
"\n",
"---\n",
"\n",
"## 📂 Step 1: Ingest a File\n",
"\n",
"In this step, we'll use `FileIngestor` to load a document. The ingestor supports various file formats including PDF, DOCX, TXT, and more.\n"
"In this step, we'll use `FileIngestor` to load a document. The ingestor supports various file formats including PDF, DOCX, TXT, and more. Writing the sample file is idempotent, so this cell can be rerun at any time.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!pip install semantica"
]
"%pip install semantica\n",
"\n",
"# spaCy models are distributed separately from the spaCy library. This lesson\n",
"# relies on the English model to recognize standalone places such as Cupertino.\n",
"import sys\n",
"import subprocess\n",
"import spacy\n",
"\n",
"try:\n",
" spacy.load(\"en_core_web_sm\")\n",
"except OSError:\n",
" subprocess.check_call([sys.executable, \"-m\", \"spacy\", \"download\", \"en_core_web_sm\"])\n"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from semantica.ingest import FileIngestor\n",
"from pathlib import Path\n",
"\n",
"# Initialize the ingestor\n",
"ingestor = FileIngestor()\n",
"from semantica.ingest import FileIngestor\n",
"\n",
"# Create a sample document for demonstration\n",
"sample_text = \"\"\"\n",
"Apple Inc. is a technology company founded by Steve Jobs, Steve Wozniak, and Ronald Wayne in 1976.\n",
"The company is headquartered in Cupertino, California.\n",
"Tim Cook is the current CEO of Apple Inc.\n",
"Apple designs and manufactures consumer electronics, software, and online services.\n",
"sample_text = \"\"\"Apple Inc. is headquartered in Cupertino, California.\n",
"In 1976, Steve Jobs founded Apple Inc.\n",
"Tim Cook is the CEO of Apple Inc.\n",
"\"\"\"\n",
"\n",
"sample_file = Path(\"sample_document.txt\")\n",
@@ -113,12 +118,14 @@
"print(f\"File: {sample_file}\")\n",
"print(f\"Content length: {len(sample_text)} characters\")\n",
"\n",
"# Ingest the file\n",
"ingestor = FileIngestor()\n",
"file_object = ingestor.ingest_file(sample_file, read_content=True)\n",
"print(f\" File name: {file_object.name}\")\n",
"print(f\" File type: {file_object.file_type}\")\n",
"print(f\" Content available: {file_object.content is not None}\")\n"
]
"print(f\" Content available: {file_object.content is not None}\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
@@ -126,64 +133,58 @@
"source": [
"## 📄 Step 2: Parse the Document\n",
"\n",
"After ingesting the file, we need to parse it to extract the text content. The `DocumentParser` handles various file formats and extracts structured content.\n"
"After ingesting the file, we need to parse it to extract the text content. `DocumentParser.parse_document()` returns the extracted text under the `\"text\"` key.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from semantica.parse import DocumentParser\n",
"\n",
"parser = DocumentParser()\n",
"# Parse the document to extract text\n",
"parsed_document = parser.parse_document(str(sample_file))\n",
"parsed_content = parsed_document.get(\"content\", \"\")\n",
"print(f\" Parsed content length: {len(parsed_content) if parsed_content else 0} characters\")\n",
"print(f\" Preview: {parsed_content[:200] if parsed_content else 'N/A'}...\")"
]
"\n",
"parsed_content = parsed_document.get(\"text\", \"\")\n",
"assert parsed_content.strip(), \"Parsing produced no text — check the input file\"\n",
"\n",
"print(f\"Parsed content length: {len(parsed_content)} characters\")\n",
"print(f\"Preview: {parsed_content[:120]}...\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## ⛏️ Step 3: Extract Entities\n",
"## ⛏️ Step 3: Extract Entities and Relations\n",
"\n",
"Now we'll extract entities from the parsed text using Named Entity Recognition (NER). This identifies people, organizations, locations, dates, and other entities in the text.\n",
"\n",
"> [!NOTE]\n",
"> In a real scenario, you would use `NERExtractor` with an LLM or model backend. Here we simulate the output for demonstration purposes.\n"
"Now we'll extract entities and relations from the parsed text. `NERExtractor` identifies people, organizations, locations and dates; `RelationExtractor` finds relations between those mentions. Both operate on the *parsed content from Step 2* — not on a copy of the raw string.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from semantica.semantic_extract import NamedEntityRecognizer, NERExtractor\n",
"from semantica.semantic_extract import NERExtractor, RelationExtractor\n",
"\n",
"ner = NamedEntityRecognizer()\n",
"extractor = NERExtractor()\n",
"ner_extractor = NERExtractor()\n",
"relation_extractor = RelationExtractor()\n",
"\n",
"print(f\"\\nText: {parsed_content[:100]}...\")\n",
"mentions = ner_extractor.extract(parsed_content)\n",
"relations = relation_extractor.extract(parsed_content, mentions)\n",
"\n",
"# Simulated extraction results\n",
"expected_entities = [\n",
" {\"text\": \"Apple Inc.\", \"type\": \"Organization\", \"start\": 0, \"end\": 10},\n",
" {\"text\": \"Steve Jobs\", \"type\": \"Person\", \"start\": 50, \"end\": 60},\n",
" {\"text\": \"Steve Wozniak\", \"type\": \"Person\", \"start\": 62, \"end\": 75},\n",
" {\"text\": \"Ronald Wayne\", \"type\": \"Person\", \"start\": 81, \"end\": 93},\n",
" {\"text\": \"1976\", \"type\": \"Date\", \"start\": 97, \"end\": 101},\n",
" {\"text\": \"Cupertino, California\", \"type\": \"Location\", \"start\": 130, \"end\": 151},\n",
" {\"text\": \"Tim Cook\", \"type\": \"Person\", \"start\": 153, \"end\": 161},\n",
"]\n",
"print(\"Entity mentions:\")\n",
"for mention in mentions:\n",
" print(f\" {mention.text!r:<13} {mention.label:<7} span=[{mention.start_char}:{mention.end_char}]\")\n",
"\n",
"for entity in expected_entities:\n",
" print(f\" - {entity['text']} ({entity['type']})\")\n"
]
"print(\"\\nExtracted relations:\")\n",
"for rel in relations:\n",
" print(f\" {rel.subject.text!r} --{rel.predicate}--> {rel.object.text!r}\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
@@ -191,58 +192,68 @@
"source": [
"## 🕸️ Step 4: Build the Knowledge Graph\n",
"\n",
"Using the extracted entities and relationships, we'll construct a knowledge graph. The graph represents entities as nodes and relationships as edges.\n"
"Using the extracted entities and relations, we construct a knowledge graph with `GraphBuilder`. Every mention gets a graph ID, and each edge is built from the actual `Relation.subject` / `Relation.object` endpoints.\n",
"\n",
"> [!NOTE]\n",
"> The graph will contain one node per *mention*, so `Apple Inc.` appears three times. Merging duplicate mentions into one canonical entity is covered in [07_Building_Knowledge_Graphs.ipynb](./07_Building_Knowledge_Graphs.ipynb).\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from semantica.kg import GraphBuilder\n",
"import networkx as nx\n",
"\n",
"entities = []\n",
"span_to_id = {}\n",
"for i, mention in enumerate(mentions, 1):\n",
" graph_id = f\"e{i}\"\n",
" span_to_id[(mention.start_char, mention.end_char)] = graph_id\n",
" entities.append({\n",
" \"id\": graph_id,\n",
" \"type\": mention.label,\n",
" \"name\": mention.text,\n",
" \"properties\": {},\n",
" })\n",
"\n",
"relationships = []\n",
"for rel in relations:\n",
" source_id = span_to_id.get((rel.subject.start_char, rel.subject.end_char))\n",
" target_id = span_to_id.get((rel.object.start_char, rel.object.end_char))\n",
" if source_id is None or target_id is None:\n",
" print(f\"Skipping relation with unmapped endpoint: \"\n",
" f\"{rel.subject.text!r} --{rel.predicate}--> {rel.object.text!r}\")\n",
" continue\n",
" relationships.append({\n",
" \"source\": source_id,\n",
" \"target\": target_id,\n",
" \"type\": rel.predicate,\n",
" \"properties\": {},\n",
" })\n",
"\n",
"builder = GraphBuilder()\n",
"knowledge_graph = builder.build({\"entities\": entities, \"relationships\": relationships})\n",
"\n",
"# Prepare data for graph construction\n",
"entities_data = [\n",
" {\"id\": f\"entity_{i}\", \"name\": entity[\"text\"], \"type\": entity[\"type\"]}\n",
" for i, entity in enumerate(expected_entities)\n",
"]\n",
"id_to_name = {entity[\"id\"]: entity[\"name\"] for entity in entities}\n",
"\n",
"relationships_data = [\n",
" {\"source\": \"entity_0\", \"target\": \"entity_1\", \"type\": \"founded_by\"},\n",
" {\"source\": \"entity_0\", \"target\": \"entity_2\", \"type\": \"founded_by\"},\n",
" {\"source\": \"entity_0\", \"target\": \"entity_3\", \"type\": \"founded_by\"},\n",
" {\"source\": \"entity_0\", \"target\": \"entity_4\", \"type\": \"founded_in\"},\n",
" {\"source\": \"entity_0\", \"target\": \"entity_5\", \"type\": \"located_in\"},\n",
" {\"source\": \"entity_6\", \"target\": \"entity_0\", \"type\": \"ceo_of\"},\n",
"]\n",
"print(f\"Nodes (entities): {len(knowledge_graph['entities'])}\")\n",
"for entity in knowledge_graph[\"entities\"]:\n",
" print(f\" {entity['id']}: {entity['name']} ({entity['type']})\")\n",
"\n",
"# Build the graph using NetworkX\n",
"kg = nx.DiGraph()\n",
"print(f\"\\nEdges (relationships): {len(knowledge_graph['relationships'])}\")\n",
"for relationship in knowledge_graph[\"relationships\"]:\n",
" print(f\" {id_to_name[relationship['source']]} \"\n",
" f\"--{relationship['type']}--> {id_to_name[relationship['target']]}\")\n",
"\n",
"for entity in entities_data:\n",
" kg.add_node(entity[\"id\"], name=entity[\"name\"], type=entity[\"type\"])\n",
"\n",
"for rel in relationships_data:\n",
" source_name = entities_data[int(rel[\"source\"].split(\"_\")[1])][\"name\"]\n",
" target_name = entities_data[int(rel[\"target\"].split(\"_\")[1])][\"name\"]\n",
" kg.add_edge(rel[\"source\"], rel[\"target\"], type=rel[\"type\"])\n",
"\n",
"print(f\" Nodes (entities): {len(kg.nodes)}\")\n",
"print(f\" Edges (relationships): {len(kg.edges)}\")\n",
"\n",
"for node_id in kg.nodes():\n",
" node_data = kg.nodes[node_id]\n",
" print(f\" Node: {node_data['name']} ({node_data['type']})\")\n",
"\n",
"for source, target, data in kg.edges(data=True):\n",
" source_name = kg.nodes[source]['name']\n",
" target_name = kg.nodes[target]['name']\n",
" print(f\" {source_name} --[{data['type']}]--> {target_name}\")\n"
]
"edges = {\n",
" (id_to_name[r[\"source\"]], r[\"type\"], id_to_name[r[\"target\"]])\n",
" for r in knowledge_graph[\"relationships\"]\n",
"}\n",
"assert (\"Apple Inc.\", \"located_in\", \"Cupertino\") in edges\n",
"assert (\"Tim Cook\", \"works_for\", \"Apple Inc.\") in edges"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
@@ -250,49 +261,81 @@
"source": [
"## 📊 Step 5: Visualize and Analyze\n",
"\n",
"Finally, we'll visualize the knowledge graph and analyze its structure. This helps you understand the relationships and entities in your data.\n"
"Finally, we render the knowledge graph with `KGVisualizer` and look at its structure. `visualize_network()` accepts the `GraphBuilder` result directly and can save an interactive HTML file.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from semantica.visualization import KGVisualizer\n",
"\n",
"visualizer = KGVisualizer()\n",
"\n",
"print(f\" Total entities: {len(kg.nodes)}\")\n",
"print(f\" Total relationships: {len(kg.edges)}\")\n",
"fig = visualizer.visualize_network(\n",
" knowledge_graph, output=\"html\", file_path=\"knowledge_graph.html\"\n",
")\n",
"print(\"Saved interactive visualization to knowledge_graph.html\")\n",
"\n",
"entity_types = {}\n",
"for node_id in kg.nodes():\n",
" entity_type = kg.nodes[node_id]['type']\n",
" entity_types[entity_type] = entity_types.get(entity_type, 0) + 1\n",
"for entity in knowledge_graph[\"entities\"]:\n",
" entity_types[entity[\"type\"]] = entity_types.get(entity[\"type\"], 0) + 1\n",
"\n",
"for etype, count in entity_types.items():\n",
" print(f\" - {etype}: {count}\")\n",
"print(\"\\nEntities by type:\")\n",
"for entity_type, count in sorted(entity_types.items()):\n",
" print(f\" - {entity_type}: {count}\")\n",
"\n",
"rel_types = {}\n",
"for _, _, data in kg.edges(data=True):\n",
" rel_type = data.get('type', 'unknown')\n",
" rel_types[rel_type] = rel_types.get(rel_type, 0) + 1\n",
"relationship_types = {}\n",
"for relationship in knowledge_graph[\"relationships\"]:\n",
" relationship_types[relationship[\"type\"]] = (\n",
" relationship_types.get(relationship[\"type\"], 0) + 1\n",
" )\n",
"\n",
"for rtype, count in rel_types.items():\n",
" print(f\" - {rtype}: {count}\")\n",
"print(\"\\nRelationships by type:\")\n",
"for relationship_type, count in sorted(relationship_types.items()):\n",
" print(f\" - {relationship_type}: {count}\")\n",
"\n",
"# Cleanup\n",
"if sample_file.exists():\n",
" sample_file.unlink()\n"
"fig"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🧹 Optional: Clean Up\n",
"\n",
"Run this cell only when you are done with the notebook. Earlier cells read `sample_document.txt`, so they stay rerunnable until you delete it here.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
"source": [
"for path in [sample_file, Path(\"knowledge_graph.html\")]:\n",
" if path.exists():\n",
" path.unlink()\n",
" print(f\"Removed {path}\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"You've built your first knowledge graph, end to end:\n",
"\n",
"- **FileIngestor** loaded the sample document\n",
"- **DocumentParser** returned its text under the `\"text\"` key\n",
"- **NERExtractor** / **RelationExtractor** produced real mentions and relations from that text\n",
"- **GraphBuilder** turned them into a graph whose edges come from the actual relation endpoints\n",
"- **KGVisualizer** rendered the result as an interactive network\n",
"\n",
"Next: merge duplicate mentions with `EntityResolver` in [07_Building_Knowledge_Graphs.ipynb](./07_Building_Knowledge_Graphs.ipynb), or explore graph metrics in the Graph Analytics notebook.\n"
]
}
],
"metadata": {
+2 -1
View File
@@ -497,7 +497,8 @@
"**Next Steps**:\n",
"* Try customizing the `NamespaceManager` to use your organization's URL.\n",
"* Explore `OntologyEvaluator` for deeper quality metrics.\n",
"* Feed the generated ontology into the **Knowledge Graph** module to start reasoning over your data!"
"* Feed the generated ontology into the **Knowledge Graph** module to start reasoning over your data!\n",
"* Put the graph, ontology, and explicit mappings together in [Semantic Layer Basics](./26_Semantic_Layer_Basics.ipynb)."
]
}
],
@@ -0,0 +1,418 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/semantica-agi/semantica/blob/main/cookbook/introduction/26_Semantic_Layer_Basics.ipynb)\n",
"\n",
"# Semantic Layer Basics: Putting the Knowledge Graph, Ontology, and Mappings Together\n",
"\n",
"## Overview\n",
"\n",
"This lesson connects three things you have already met — a knowledge graph, an ontology, and RDF export — into one minimal *semantic layer*: a knowledge graph whose types, relationships, and properties are **explicitly mapped** to ontology terms, so the resulting RDF can be queried with SPARQL against a shared vocabulary.\n",
"\n",
"**Documentation**: [API Reference](https://semantica.readthedocs.io/concepts/)\n",
"\n",
"### 🎯 Learning Objectives\n",
"\n",
"- Build a small knowledge graph with `GraphBuilder`\n",
"- Generate a starter ontology from the graph with `OntologyGenerator`\n",
"- Write **explicit** entity-type, relationship-type, and property mappings to ontology terms\n",
"- Produce ontology-aligned RDF and store it with `TripletStore`\n",
"- Answer a business question with one small SPARQL query\n",
"\n",
"### 📚 Prerequisites\n",
"\n",
"- [07_Building_Knowledge_Graphs.ipynb](./07_Building_Knowledge_Graphs.ipynb) — graphs from entities and relationships\n",
"- [14_Ontology.ipynb](./14_Ontology.ipynb) — ontology generation\n",
"- [20_Triplet_Store.ipynb](./20_Triplet_Store.ipynb) — triplet store backends\n",
"\n",
"> [!NOTE]\n",
"> **Teaching mappings vs. governed mappings.** The mappings in this lesson are a demo: they live in a Python dict and are derived from a generated ontology. A production semantic layer uses governed identifiers, hand-designed ontologies, explicit source mappings, validation (SHACL), provenance, and versioning — that workflow is covered in [Advanced: Manual Ontology + Snowflake Mapping](../advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb).\n",
"\n",
"## Installation\n",
"\n",
"The triplet-store step uses the embedded Oxigraph backend, so install with that extra. Pin at least 0.6.7: earlier releases could generate ontology classes with no URI (#1103), which silently breaks the mappings below instead of failing loudly.\n",
"\n",
"```bash\n",
"pip install \"semantica[tripletstore-oxigraph]>=0.6.7\"\n",
"```\n",
"\n",
"---\n",
"\n",
"## Step 1: Build a Knowledge Graph\n",
"\n",
"Start from a small, explicit set of entities and relationships — two people, an organization, and a project.\n"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"!pip install \"semantica[tripletstore-oxigraph]>=0.6.7\"\n"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {},
"source": [
"from semantica.kg import GraphBuilder\n",
"\n",
"entities = [\n",
" {\"id\": \"e1\", \"type\": \"Person\", \"name\": \"Alice\", \"properties\": {\"age\": 30, \"role\": \"Engineer\"}},\n",
" {\"id\": \"e2\", \"type\": \"Person\", \"name\": \"Bob\", \"properties\": {\"age\": 35, \"role\": \"Manager\"}},\n",
" {\"id\": \"e3\", \"type\": \"Organization\", \"name\": \"Tech Corp\", \"properties\": {\"founded\": 2010}},\n",
" {\"id\": \"e4\", \"type\": \"Project\", \"name\": \"Project Alpha\", \"properties\": {\"status\": \"active\"}},\n",
"]\n",
"\n",
"relationships = [\n",
" {\"source\": \"e1\", \"target\": \"e2\", \"type\": \"reports_to\", \"properties\": {}},\n",
" {\"source\": \"e1\", \"target\": \"e3\", \"type\": \"works_for\", \"properties\": {}},\n",
" {\"source\": \"e2\", \"target\": \"e3\", \"type\": \"works_for\", \"properties\": {}},\n",
" {\"source\": \"e1\", \"target\": \"e4\", \"type\": \"works_on\", \"properties\": {}},\n",
"]\n",
"\n",
"builder = GraphBuilder()\n",
"knowledge_graph = builder.build({\"entities\": entities, \"relationships\": relationships})\n",
"\n",
"id_to_name = {entity[\"id\"]: entity[\"name\"] for entity in entities}\n",
"\n",
"print(f\"Entities ({len(knowledge_graph['entities'])}):\")\n",
"for entity in knowledge_graph[\"entities\"]:\n",
" print(f\" {entity['id']}: {entity['name']} ({entity['type']}) {entity['properties']}\")\n",
"\n",
"print(f\"\\nRelationships ({len(knowledge_graph['relationships'])}):\")\n",
"for relationship in knowledge_graph[\"relationships\"]:\n",
" print(f\" {id_to_name[relationship['source']]} \"\n",
" f\"--{relationship['type']}--> {id_to_name[relationship['target']]}\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Generate a Starter Ontology\n",
"\n",
"`OntologyGenerator` infers OWL classes and properties from graph records. Because `GraphBuilder` keeps business attributes inside each entity's `properties` dictionary while ontology inference reads record fields, we first create a flat **inference view**. The knowledge graph itself remains unchanged. Two settings matter here:\n",
"\n",
"- `base_uri` puts every generated term in *your* namespace\n",
"- `min_occurrences=1` includes classes that occur only once (the default of 2 would drop `Organization` and `Project` from this tiny demo graph)\n",
"\n",
"Note that the generator normalizes names: the relationship type `works_for` becomes the ontology property `worksFor`. That is exactly why the next step maps terms **explicitly** instead of matching names.\n"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"from semantica.ontology import OntologyGenerator\n",
"\n",
"BASE_URI = \"https://example.org/company/\"\n",
"\n",
"# Adapt the property-graph representation to the record shape consumed by\n",
"# OntologyGenerator, so age/role/founded/status become declared properties.\n",
"ontology_input = {\n",
" \"entities\": [\n",
" {\n",
" **{key: value for key, value in entity.items() if key != \"properties\"},\n",
" **entity.get(\"properties\", {}),\n",
" }\n",
" for entity in knowledge_graph[\"entities\"]\n",
" ],\n",
" \"relationships\": knowledge_graph[\"relationships\"],\n",
"}\n",
"\n",
"generator = OntologyGenerator(base_uri=BASE_URI, min_occurrences=1)\n",
"ontology = generator.generate_from_graph(ontology_input)\n",
"\n",
"# OntologyGenerator calls datatype properties `data`; TripletStore's public\n",
"# ontology contract calls them `datatype`. Normalize that boundary explicitly.\n",
"store_ontology = {\n",
" **ontology,\n",
" \"properties\": [\n",
" {**prop, \"type\": \"datatype\" if prop[\"type\"] == \"data\" else prop[\"type\"]}\n",
" for prop in ontology[\"properties\"]\n",
" ],\n",
"}\n",
"\n",
"print(\"Classes:\")\n",
"for ontology_class in ontology[\"classes\"]:\n",
" print(f\" {ontology_class['name']:<14} {ontology_class['uri']}\")\n",
"\n",
"print(\"\\nProperties:\")\n",
"for prop in ontology[\"properties\"]:\n",
" print(f\" {prop['name']:<14} {prop['type']:<7} {prop['uri']} \"\n",
" f\"(domain={prop['domain']}, range={prop['range']})\")\n",
"\n",
"assert len(ontology[\"classes\"]) == 3"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Map the Graph to Ontology Terms\n",
"\n",
"The heart of a semantic layer is the mapping contract: which source type, relationship, and property corresponds to which ontology term.\n",
"\n",
"- **Entity types** and **relationship types**: each generated class/property records the source name it was inferred from (`metadata[\"inferred_from\"]`), so the mapping is read off the ontology itself — no fragile name matching between `works_for` and `worksFor`.\n",
"- **Properties**: the flat inference view makes `name`, `age`, `role`, `founded`, and `status` real generated datatype properties. Every mapping therefore points to a term declared in the ontology — no URI is invented only at mapping time.\n"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"entity_type_mappings = {\n",
" ontology_class[\"metadata\"][\"inferred_from\"]: ontology_class[\"uri\"]\n",
" for ontology_class in ontology[\"classes\"]\n",
"}\n",
"\n",
"relationship_type_mappings = {\n",
" prop[\"metadata\"][\"inferred_from\"]: prop[\"uri\"]\n",
" for prop in ontology[\"properties\"]\n",
" if prop[\"type\"] == \"object\"\n",
"}\n",
"\n",
"datatype_property_uris = {\n",
" prop[\"metadata\"][\"inferred_from\"]: prop[\"uri\"]\n",
" for prop in ontology[\"properties\"]\n",
" if prop[\"type\"] != \"object\"\n",
"}\n",
"\n",
"property_mappings = datatype_property_uris\n",
"\n",
"semantic_layer = {\n",
" \"graph\": knowledge_graph,\n",
" \"ontology\": ontology,\n",
" \"mappings\": {\n",
" \"entity_type_mappings\": entity_type_mappings,\n",
" \"relationship_type_mappings\": relationship_type_mappings,\n",
" \"property_mappings\": property_mappings,\n",
" },\n",
"}\n",
"\n",
"for mapping_name, mapping in semantic_layer[\"mappings\"].items():\n",
" print(f\"{mapping_name}:\")\n",
" for source, target in mapping.items():\n",
" print(f\" {source:<12} -> {target}\")\n",
"\n",
"# Every type and relationship in the graph must have an ontology term\n",
"assert set(entity_type_mappings) == {entity[\"type\"] for entity in entities}\n",
"assert set(relationship_type_mappings) == {rel[\"type\"] for rel in relationships}\n",
"assert set(property_mappings) == {\"name\", \"age\", \"role\", \"founded\", \"status\"}\n",
"assert set(property_mappings.values()) <= {prop[\"uri\"] for prop in ontology[\"properties\"]}"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Apply the Mappings\n",
"\n",
"Applying the semantic layer means rewriting the graph so every type, relationship, and property key is an ontology term. This *aligned* graph — not the original one — is what gets exported and stored.\n"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"aligned_graph = {\n",
" \"entities\": [\n",
" {\n",
" **entity,\n",
" \"type\": entity_type_mappings[entity[\"type\"]],\n",
" \"properties\": {\n",
" property_mappings[\"name\"]: entity[\"name\"],\n",
" **{\n",
" property_mappings[key]: value\n",
" for key, value in entity[\"properties\"].items()\n",
" },\n",
" },\n",
" }\n",
" for entity in knowledge_graph[\"entities\"]\n",
" ],\n",
" \"relationships\": [\n",
" {**rel, \"type\": relationship_type_mappings[rel[\"type\"]]}\n",
" for rel in knowledge_graph[\"relationships\"]\n",
" ],\n",
"}\n",
"\n",
"print(\"Aligned entity sample:\")\n",
"sample = aligned_graph[\"entities\"][0]\n",
"print(f\" id: {sample['id']}\")\n",
"print(f\" type: {sample['type']}\")\n",
"for key, value in sample[\"properties\"].items():\n",
" print(f\" {key} = {value}\")\n",
"\n",
"print(\"\\nAligned relationship sample:\")\n",
"print(f\" {aligned_graph['relationships'][0]['type']}\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 5: Store and Export Complete Ontology-Aligned RDF\n",
"\n",
"`TripletStore.store()` materializes both the ontology declarations and the aligned instance graph. We then read those triples through the store's public API and serialize that complete RDF graph as Turtle. This avoids the compact `RDFExporter` entity projection, which does not include arbitrary entries from an entity's `properties` dictionary.\n"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"from rdflib import Graph, Literal, URIRef\n",
"from rdflib.namespace import OWL, RDF\n",
"from semantica.triplet_store import TripletStore\n",
"\n",
"store = TripletStore(backend=\"oxigraph\")\n",
"result = store.store(aligned_graph, store_ontology)\n",
"print(f\"Stored triples: {result['processed']} (failed: {result['failed']})\")\n",
"\n",
"rdf_graph = Graph()\n",
"for triplet in store.get_triplets():\n",
" datatype = triplet.metadata.get(\"datatype\")\n",
" if datatype:\n",
" object_term = Literal(triplet.object, datatype=URIRef(datatype))\n",
" elif triplet.object.startswith((\"http://\", \"https://\", \"urn:\")):\n",
" object_term = URIRef(triplet.object)\n",
" else:\n",
" object_term = Literal(triplet.object)\n",
" rdf_graph.add((URIRef(triplet.subject), URIRef(triplet.predicate), object_term))\n",
"\n",
"rdf_graph.serialize(destination=\"semantic_layer.ttl\", format=\"turtle\")\n",
"turtle = open(\"semantic_layer.ttl\", encoding=\"utf-8\").read()\n",
"print(turtle[:600])\n",
"\n",
"# The exported RDF contains declarations plus mapped instance facts.\n",
"declared_datatype_properties = {\n",
" str(subject) for subject in rdf_graph.subjects(RDF.type, OWL.DatatypeProperty)\n",
"}\n",
"assert result[\"failed\"] == 0\n",
"assert set(property_mappings.values()) <= declared_datatype_properties\n",
"assert (\n",
" URIRef(BASE_URI + \"e1\"),\n",
" URIRef(property_mappings[\"role\"]),\n",
" Literal(\"Engineer\"),\n",
") in rdf_graph\n",
"assert (\n",
" URIRef(BASE_URI + \"e1\"),\n",
" URIRef(relationship_type_mappings[\"works_for\"]),\n",
" URIRef(BASE_URI + \"e3\"),\n",
") in rdf_graph\n",
"print(\"... exported semantic_layer.ttl\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 6: Query the Semantic Layer\n",
"\n",
"The embedded Oxigraph backend runs in memory, so there is nothing to start beyond installing the `tripletstore-oxigraph` extra. The organization is constrained by its mapped `name` predicate; the query therefore means *Tech Corp*, rather than accidentally matching employees of every organization.\n"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"query = f\"\"\"\n",
"SELECT ?name ?role WHERE {{\n",
" ?person <{BASE_URI}worksFor> ?org .\n",
" ?org <{BASE_URI}name> \"Tech Corp\" .\n",
" ?person <{BASE_URI}name> ?name .\n",
" ?person <{BASE_URI}role> ?role .\n",
"}}\n",
"ORDER BY ?name\n",
"\"\"\"\n",
"query_result = store.execute_query(query)\n",
"\n",
"print(\"\\nWho works for Tech Corp, and in which role?\")\n",
"for binding in query_result.bindings:\n",
" print(f\" {binding['name']['value']} — {binding['role']['value']}\")\n",
"\n",
"assert [(row[\"name\"][\"value\"], row[\"role\"][\"value\"]) for row in query_result.bindings] == [\n",
" (\"Alice\", \"Engineer\"),\n",
" (\"Bob\", \"Manager\"),\n",
"]"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🧹 Optional: Clean Up\n"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"from pathlib import Path\n",
"\n",
"ttl_file = Path(\"semantic_layer.ttl\")\n",
"if ttl_file.exists():\n",
" ttl_file.unlink()\n",
" print(f\"Removed {ttl_file}\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"A minimal semantic layer is a composition, and you have now built each part:\n",
"\n",
"1. **Knowledge graph** — `GraphBuilder` from explicit entities and relationships\n",
"2. **Ontology** — `OntologyGenerator` with your `base_uri`\n",
"3. **Explicit mappings** — entity types, relationship types, and properties, each tied to an ontology term\n",
"4. **Ontology-aligned RDF** — the mappings applied to the graph, materialized with `TripletStore`, and serialized to Turtle from the store's own triples\n",
"5. **Queryable store** — `TripletStore` (embedded Oxigraph) answering a SPARQL question over the shared vocabulary\n",
"\n",
"### Where to go next\n",
"\n",
"The production version of this workflow — hand-designed governed ontologies, explicit source-to-ontology mappings from a warehouse, n-ary modeling, SHACL validation, provenance, and versioning — is covered in [Advanced: Manual Ontology + Snowflake Mapping](../advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb).\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+4 -4
View File
@@ -185,7 +185,7 @@ Centralized `ConfigManager` with environment variable overrides. No magic defaul
| **Deduplication v2** | `blocking_v2`, `hybrid_v2`, `semantic_v2`: up to 7x faster than v1 |
| **Indexed search** | Explorer search at 0.004ms on 118k nodes (v0.5.0) |
- [Modules](modules) — Full module documentation with code examples.
- [Learning More](learning-more) — Configuration reference, performance guide, and troubleshooting.
- [Pipeline Reference](reference/pipeline) — Pipeline orchestration, workers, and retry policies.
- [Core Reference](reference/core) — Framework lifecycle, plugin registry, and configuration.
- [Modules](/modules) — Full module documentation with code examples.
- [Learning More](/learning-more) — Configuration reference, performance guide, and troubleshooting.
- [Pipeline Reference](/reference/pipeline) — Pipeline orchestration, workers, and retry policies.
- [Core Reference](/reference/core) — Framework lifecycle, plugin registry, and configuration.
+14 -14
View File
@@ -5,7 +5,7 @@ icon: "compass"
---
<Info>
Every module works independently — import only what you need. This page maps developer goals to starting points. The [Module Reference](modules) covers every module in depth.
Every module works independently — import only what you need. This page maps developer goals to starting points. The [Module Reference](/modules) covers every module in depth.
</Info>
## Quick Reference
@@ -89,7 +89,7 @@ Pick your goal to see the minimum imports and a working skeleton.
Pass `method="pattern"` to `NERExtractor` for zero-cost, zero-API-key extraction. Switch to `method="llm"` with any of the supported providers for higher recall.
</Tip>
**Next:** [Quickstart →](quickstart) — full pipeline with visualization and export.
**Next:** [Quickstart →](/quickstart) — full pipeline with visualization and export.
</Tab>
<Tab title="Build GraphRAG">
@@ -122,7 +122,7 @@ Pick your goal to see the minimum imports and a working skeleton.
print(result["reasoning_path"]) # multi-hop trace
```
**Next:** [Context module reference →](reference/context)
**Next:** [Context module reference →](/reference/context)
</Tab>
<Tab title="Add Agent Memory">
@@ -163,7 +163,7 @@ Pick your goal to see the minimum imports and a working skeleton.
`decision_tracking=True` is required. Without it, `record_decision()` raises `RuntimeError`.
</Note>
**Next:** [Context module reference →](reference/context)
**Next:** [Context module reference →](/reference/context)
</Tab>
<Tab title="Track Provenance">
@@ -195,7 +195,7 @@ Pick your goal to see the minimum imports and a working skeleton.
diff = manager.diff("v1.0", "v1.1")
```
**Next:** [Provenance reference →](reference/provenance) · [Change Management reference →](reference/change_management)
**Next:** [Provenance reference →](/reference/provenance) · [Change Management reference →](/reference/change_management)
</Tab>
<Tab title="Export">
@@ -222,11 +222,11 @@ Pick your goal to see the minimum imports and a working skeleton.
**Formats:** Turtle · JSON-LD · N-Triples · RDF/XML · Parquet · Cypher · Arrow · OWL · CSV · ArangoDB AQL
**Next:** [Export module reference →](reference/export)
**Next:** [Export module reference →](/reference/export)
</Tab>
<Tab title="MCP — Claude / Cursor">
Use Semantica from Claude Desktop, Cursor, VS Code, or any MCP-aware tool — no Python code required after setup. 12 tools available instantly.
Use Semantica from Claude Desktop, Cursor, VS Code, or any MCP-aware tool — no Python code required after setup. 15 tools available instantly.
**Step 1 — Install:**
```bash
@@ -268,7 +268,7 @@ Pick your goal to see the minimum imports and a working skeleton.
Set `SEMANTICA_KG_PATH` to persist your graph across restarts. Without it, all data is lost when the server process exits.
</Warning>
**Next:** [MCP Server reference →](reference/mcp_server)
**Next:** [MCP Server reference →](/reference/mcp_server)
</Tab>
</Tabs>
@@ -283,11 +283,11 @@ Pick your goal to see the minimum imports and a working skeleton.
Use **both together** via `AgentContext` (GraphRAG) to get grounded LLM responses where every claim traces back to a source node.
See also: [Core Concepts](concepts)
See also: [Core Concepts](/concepts)
</Accordion>
<Accordion title="I just want to run something quickly." icon="rocket">
Start with the [Quickstart](quickstart). It builds a complete pipeline (ingest → parse → extract → graph → visualize → export) with no API key required.
Start with the [Quickstart](/quickstart). It builds a complete pipeline (ingest → parse → extract → graph → visualize → export) with no API key required.
</Accordion>
<Accordion title="I'm adding Semantica to an existing agent — what's the minimum?" icon="plug">
@@ -304,7 +304,7 @@ Pick your goal to see the minimum imports and a working skeleton.
)
```
[Context module reference →](reference/context)
[Context module reference →](/reference/context)
</Accordion>
<Accordion title="I need a compliance-ready pipeline — what's the minimum stack?" icon="shield-check">
@@ -322,6 +322,6 @@ Pick your goal to see the minimum imports and a working skeleton.
---
- [Quickstart](quickstart) — Full pipeline in 5 minutes.
- [Module Reference](modules) — Every module with examples and common chains.
- [API Reference](reference/context) — Complete class and method documentation.
- [Quickstart](/quickstart) — Full pipeline in 5 minutes.
- [Module Reference](/modules) — Every module with examples and common chains.
- [API Reference](/reference/context) — Complete class and method documentation.
+2 -2
View File
@@ -48,5 +48,5 @@ Published research using Semantica? [Let us know](https://github.com/semantica-a
## See Also
- [License](project-license) — MIT License details.
- [Community](community) — Connect with the Semantica community.
- [License](/project-license) — MIT License details.
- [Community](/community) — Connect with the Semantica community.
+9 -9
View File
@@ -24,7 +24,7 @@ After installation the following commands are available:
| `semantica-mcp` | `semantica.mcp_server:main` | MCP server (stdio) for Claude Desktop, Cursor, Windsurf, and other MCP clients |
<Note>
`semantica-explorer` requires `pip install semantica[explorer]`. Running it without that extra will immediately print an error and exit. See [Explorer Setup](explorer-setup) for the full walkthrough.
`semantica-explorer` requires `pip install semantica[explorer]`. Running it without that extra will immediately print an error and exit. See [Explorer Setup](/explorer-setup) for the full walkthrough.
</Note>
@@ -52,8 +52,8 @@ python -c "import semantica; print(semantica.__version__)"
- **semantica** — The general-purpose CLI. Use it for one-off pipeline runs, entity extraction, and graph operations from a shell script or CI job.
- **semantica-server** — Starts the REST API server. Binds to `0.0.0.0:8000`. Use this when another service or application needs programmatic access to Semantica over HTTP.
- **semantica-worker** — Background task processor. Run alongside `semantica-server` when you need async pipeline execution outside the request cycle. Start the server first, then start one or more workers pointing at the same backend.
- **semantica-explorer** — Launches the browser dashboard. Requires `pip install semantica[explorer]`. Use this to explore a saved knowledge graph interactively. See [Explorer Setup](explorer-setup).
- **semantica-mcp** — Runs the MCP server over stdio. Configure it in your MCP client's settings file to expose all 12 tools and 3 resources to Claude Desktop, Cursor, Windsurf, or any MCP-aware client. See [MCP Server](reference/mcp_server).
- **semantica-explorer** — Launches the browser dashboard. Requires `pip install semantica[explorer]`. Use this to explore a saved knowledge graph interactively. See [Explorer Setup](/explorer-setup).
- **semantica-mcp** — Runs the MCP server over stdio. Configure it in your MCP client's settings file to expose all 15 tools and 3 resources to Claude Desktop, Cursor, Windsurf, or any MCP-aware client. See [MCP Server](/reference/mcp_server).
## Usage Examples
@@ -116,7 +116,7 @@ python -c "import semantica; print(semantica.__version__)"
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | semantica-mcp
```
You should receive a JSON-RPC response. See [MCP Server](reference/mcp_server) for the full list of tools and resources.
You should receive a JSON-RPC response. See [MCP Server](/reference/mcp_server) for the full list of tools and resources.
</Tab>
<Tab title="Explorer">
```bash
@@ -124,7 +124,7 @@ python -c "import semantica; print(semantica.__version__)"
semantica-explorer --graph my_graph.json
```
See [Explorer Setup](explorer-setup) for the full walkthrough including how to build and save a graph file.
See [Explorer Setup](/explorer-setup) for the full walkthrough including how to build and save a graph file.
</Tab>
<Tab title="Python module form">
Every command also runs as a Python module: useful when the script directory is not on `PATH`:
@@ -228,7 +228,7 @@ Install the [Microsoft Visual C++ Redistributable](https://aka.ms/vs/17/release/
## Next Steps
- [Explorer Setup](explorer-setup) — Build a graph, save it, and launch the browser dashboard.
- [MCP Server](reference/mcp_server) — All 12 tools and 3 resources exposed over the MCP protocol.
- [Installation](installation) — Virtual environments, optional extras, and platform-specific notes.
- [Quickstart](quickstart) — End-to-end pipeline walkthrough with working code.
- [Explorer Setup](/explorer-setup) — Build a graph, save it, and launch the browser dashboard.
- [MCP Server](/reference/mcp_server) — All 15 tools and 3 resources exposed over the MCP protocol.
- [Installation](/installation) — Virtual environments, optional extras, and platform-specific notes.
- [Quickstart](/quickstart) — End-to-end pipeline walkthrough with working code.
+2 -2
View File
@@ -109,12 +109,12 @@ def my_ingestor(source):
method_registry.register("file", "my_format", my_ingestor)
```
See [Architecture](architecture#extension-points) for the full extension guide.
See [Architecture](/architecture#extension-points) for the full extension guide.
## How to Contribute
- [Contributing Guide](contributing-guide) — Submit code, documentation, tests, or cookbook notebooks.
- [Contributing Guide](/contributing-guide) — Submit code, documentation, tests, or cookbook notebooks.
- [GitHub Issues](https://github.com/semantica-agi/semantica/issues) — Report bugs, request features, or propose integrations.
- [Discord](https://discord.gg/sV34vps5hH) — Share what you're building with the community.
- [GitHub Discussions](https://github.com/semantica-agi/semantica/discussions) — Long-form questions, design discussions, and ideas.
+5 -5
View File
@@ -55,7 +55,7 @@ There's no single right way to contribute. Pick the path that fits your skills a
- Review open pull requests
- Share your Semantica projects in GitHub Discussions
See the [Contributing Guide](contributing-guide) for the full development workflow.
See the [Contributing Guide](/contributing-guide) for the full development workflow.
## Stay Connected
@@ -68,7 +68,7 @@ See the [Contributing Guide](contributing-guide) for the full development workfl
## See Also
- [Contributing Guide](contributing-guide) — Step-by-step guide for submitting PRs and setting up your dev environment.
- [Community Projects](community-projects) — Projects and integrations built by the community.
- [FAQ](faq) — Common questions answered.
- [Governance](governance) — How the project is run and decisions are made.
- [Contributing Guide](/contributing-guide) — Step-by-step guide for submitting PRs and setting up your dev environment.
- [Community Projects](/community-projects) — Projects and integrations built by the community.
- [FAQ](/faq) — Common questions answered.
- [Governance](/governance) — How the project is run and decisions are made.
+7 -7
View File
@@ -5,7 +5,7 @@ icon: "book-open"
---
<Info>
New here? Start with [Getting Started](getting-started) for hands-on examples, then return here for deeper understanding.
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.
@@ -203,7 +203,7 @@ ontology = {
}
```
Semantica can auto-generate ontologies from your knowledge graph or import existing OWL/RDF/Turtle ontologies. The **Ontology Hub** (v0.5.0) adds a visual editor, SHACL Studio, alignment authoring, and a live health dashboard. See the [Ontology reference](reference/ontology) for the full 6-stage generation pipeline.
Semantica can auto-generate ontologies from your knowledge graph or import existing OWL/RDF/Turtle ontologies. The **Ontology Hub** (v0.5.0) adds a visual editor, SHACL Studio, alignment authoring, and a live health dashboard. See the [Ontology reference](/reference/ontology) for the full 6-stage generation pipeline.
## Reasoning & Inference
@@ -319,7 +319,7 @@ scores = calc.calculate_similarity(entity_a, entity_b)
**Features:** N×N semantic distance matrices, ego-mode visualization, distance band classification (`near` / `mid` / `far`), embedding cache optimization for large graphs.
The [Visualization module](reference/visualization) renders distance matrices as interactive heatmaps and ego-mode neighborhood graphs. The [Explorer](reference/explorer) embeds distance intelligence directly in the browser dashboard.
The [Visualization module](/reference/visualization) renders distance matrices as interactive heatmaps and ego-mode neighborhood graphs. The [Explorer](/reference/explorer) embeds distance intelligence directly in the browser dashboard.
## Deduplication & Entity Resolution
@@ -413,7 +413,7 @@ When multiple sources disagree on the same fact, Semantica flags and resolves th
- **Majority vote**: aggregate across all sources with ≥ 2 agreeing
- **Manual review**: flag for human arbitration; continue pipeline without blocking
See the [Conflicts reference](reference/conflicts) for `ConflictResolver`, `SourceTracker`, and `InvestigationGuideGenerator`.
See the [Conflicts reference](/reference/conflicts) for `ConflictResolver`, `SourceTracker`, and `InvestigationGuideGenerator`.
## Custom Plugin Development
@@ -482,6 +482,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.
+2 -2
View File
@@ -85,5 +85,5 @@ All contributors are expected to follow the [Contributor Covenant Code of Conduc
- [GitHub Discussions](https://github.com/semantica-agi/semantica/discussions)
- [Discord](https://discord.gg/sV34vps5hH)
- [Community](community) — Community guidelines and values.
- [Governance](governance) — How decisions are made and the project is run.
- [Community](/community) — Community guidelines and values.
- [Governance](/governance) — How decisions are made and the project is run.
+2 -1
View File
@@ -8,7 +8,7 @@ icon: "flask"
**Where to start:**
- **New to Semantica**: begin with [Core Tutorials](#core-tutorials)
- **Building an application**: see [Advanced Concepts](#advanced-concepts)
- **Need installation help**: see the [Installation Guide](installation)
- **Need installation help**: see the [Installation Guide](/installation)
</Tip>
<Note>
@@ -36,6 +36,7 @@ Essential guides to master the Semantica framework.
- **[Graph Store](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/09_Graph_Store.ipynb)** — Persisting knowledge graphs in Neo4j or FalkorDB. Topics: Neo4j, Cypher, Persistence · *Intermediate*
- **[Ontology](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/14_Ontology.ipynb)** — Defining domain schemas and ontologies to structure your data. Topics: OWL, RDF, Schema Design · *Intermediate*
- **[Seed Data](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/25_Seed_Data.ipynb)** — Bootstrapping a knowledge graph from trusted CSV, JSON, database, and API sources before extraction runs. Topics: SeedDataManager, Foundation Graphs · *Intermediate*
- **[Semantic Layer Basics](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/26_Semantic_Layer_Basics.ipynb)** — Capstone tutorial that combines a knowledge graph, generated ontology, explicit mappings, ontology-aligned RDF, and a SPARQL query. Topics: Semantic Layer, Ontology Mapping, Oxigraph, SPARQL · *Intermediate*
## Advanced Concepts
+6 -6
View File
@@ -6,7 +6,7 @@ icon: "map"
**`semantica-explorer`** is an **interactive browser dashboard** for knowledge graph exploration. You give it a graph file, it starts a local server, and opens a browser tab where you can search nodes, find paths, inspect provenance, and run analytics: no code required after launch.
This page covers everything needed to go from zero to a running Explorer. For the full REST API reference and endpoint catalogue, see [Explorer Reference](reference/explorer).
This page covers everything needed to go from zero to a running Explorer. For the full REST API reference and endpoint catalogue, see [Explorer Reference](/reference/explorer).
## Prerequisites
@@ -27,7 +27,7 @@ Verify:
semantica-explorer --help
```
You should see the usage message with the four available flags. If you see `command not found`, activate your virtual environment first. See [CLI Setup](cli-setup#troubleshooting) for PATH help.
You should see the usage message with the four available flags. If you see `command not found`, activate your virtual environment first. See [CLI Setup](/cli-setup#troubleshooting) for PATH help.
## Minimal End-to-End Example
@@ -264,7 +264,7 @@ Once running, Explorer exposes a REST API and dashboard for:
The full endpoint catalogue is documented in the Swagger UI at `/docs` and in the reference page below.
- [Explorer Reference](reference/explorer) — Every REST endpoint, WebSocket events, analytics, and all supported flags.
- [CLI Setup](cli-setup) — All five Semantica executables and when to use each one.
- [Context Module](reference/context) — Full documentation for ContextGraph: build, query, save, and load.
- [Quickstart](quickstart) — End-to-end pipeline: ingest → extract → build graph → export.
- [Explorer Reference](/reference/explorer) — Every REST endpoint, WebSocket events, analytics, and all supported flags.
- [CLI Setup](/cli-setup) — All five Semantica executables and when to use each one.
- [Context Module](/reference/context) — Full documentation for ContextGraph: build, query, save, and load.
- [Quickstart](/quickstart) — End-to-end pipeline: ingest → extract → build graph → export.
+8 -8
View File
@@ -16,7 +16,7 @@ icon: "circle-question"
| Python version? | 3.8+ (3.11+ recommended) |
| API key required? | Optional: pattern extraction works with no keys |
| Works with LangChain / LlamaIndex? | Yes: Semantica is a layer on top, not a replacement |
| Production-ready? | Yes: 1,000+ tests, v0.5.0 ships with 12 security fixes |
| Production-ready? | Yes: 1,000+ tests, security fixes shipped in every release (see [CHANGELOG](https://github.com/semantica-agi/semantica/blob/main/CHANGELOG.md)) |
| Latest version? | **v0.6.7** (August 2026) |
| Local LLMs? | Yes: Ollama via LiteLLM, HuggingFaceLLM for air-gapped |
@@ -70,9 +70,9 @@ Yes: MIT licensed, no vendor lock-in, no paywalled features. Some capabilities r
<Accordion title="What's the latest version?" icon="star">
**v0.5.0**: released May 2026.
**v0.6.7**: released August 2026.
Highlights: Ontology Hub, Distance Intelligence, Parquet/XML ingestion, 12 security fixes, Graph Explorer redesign, NER gateway fix.
Highlights: first-class LangChain integration, SAP OData ingestor, human-editable Markdown round-trip persistence for `ContextGraph`, a structured Action layer for the reasoning engine, and a public `run_shacl_validation` entry point. The 0.6.x line also added first-class CrewAI support and the Semantica RDF vocabulary with deterministic IRIs. See the [CHANGELOG](https://github.com/semantica-agi/semantica/blob/main/CHANGELOG.md) for the full history.
```bash
pip install --upgrade semantica
@@ -93,7 +93,7 @@ pip install --upgrade semantica
pip install semantica
```
See [Installation](installation) for virtual environment setup, optional extras (`[gpu]`, `[all]`, provider-specific), and platform-specific troubleshooting.
See [Installation](/installation) for virtual environment setup, optional extras (`[gpu]`, `[all]`, provider-specific), and platform-specific troubleshooting.
</Accordion>
@@ -173,7 +173,7 @@ This includes PyTorch with CUDA, FAISS GPU, and CuPy.
<Accordion title="How does Semantica handle large datasets?" icon="layer-group">
- **Batching**: process documents in configurable chunks to control memory usage
- **Parallel processing**: `Pipeline(workers=N)` runs extraction steps concurrently
- **Parallel processing**: the `semantica.pipeline` module can run independent, parallel-safe steps in the same dependency layer concurrently (see the [Pipeline guide](/guides/pipeline))
- **Delta processing**: update graphs incrementally without full recompute on new data
- **Persistent backends**: swap in-memory NetworkX for Neo4j, FalkorDB, or Apache AGE for large-scale production graphs
@@ -269,13 +269,13 @@ Groq, OpenAI, Anthropic, Google Gemini, Ollama (fully local), DeepSeek, Novita A
<Accordion title="Is Semantica production-ready?" icon="shield-check">
Yes. v0.5.0 ships with:
Yes. Every release ships with:
- 1,000+ passing tests across Python 3.83.12
- `PipelineValidator` and `FailureHandler` with exponential backoff and configurable retry policies
- W3C PROV-O provenance tracking across all modules
- Change management with SHA-256 checksums and full audit trails
- 12 security vulnerability fixes: eval injection, pickle deserialization, SQL injection, XXE, SSRF, ReDoS, path traversal, and more
- Ongoing security hardening: eval injection, pickle deserialization, SQL injection, XXE, SSRF, ReDoS, and path traversal fixes have all landed across recent releases (see the [CHANGELOG](https://github.com/semantica-agi/semantica/blob/main/CHANGELOG.md) security sections)
</Accordion>
@@ -350,4 +350,4 @@ set PYTHONIOENCODING=utf-8
- [Discord](https://discord.gg/sV34vps5hH) — Community chat and live support.
- [GitHub Issues](https://github.com/semantica-agi/semantica/issues) — Bug reports and feature requests.
- [Contributing](contributing-guide) — Help improve Semantica.
- [Contributing](/contributing-guide) — Help improve Semantica.
+23 -23
View File
@@ -5,7 +5,7 @@ icon: "rocket"
---
<Tip>
Already installed? Jump straight to [Quickstart](quickstart). Need setup help first? See [Installation](installation).
Already installed? Jump straight to [Quickstart](/quickstart). Need setup help first? See [Installation](/installation).
</Tip>
## What You Can Build
@@ -52,15 +52,15 @@ icon: "rocket"
| Track | You want to... | Start with |
| :----- | :-------------- | :--------- |
| **Knowledge Graph** | Turn documents into structured, queryable graphs | [Quickstart → Step 1](quickstart) |
| **Agent Context** | Give your AI agent persistent memory and decision tracking | [Context reference](reference/context) |
| **GraphRAG** | Ground LLM answers in structured knowledge | [Concepts → GraphRAG](concepts#graphrag) |
| **MCP Integration** | Use Semantica from Claude Desktop or VS Code | [MCP Server](reference/mcp_server) |
| **Knowledge Graph** | Turn documents into structured, queryable graphs | [Quickstart → Step 1](/quickstart) |
| **Agent Context** | Give your AI agent persistent memory and decision tracking | [Context reference](/reference/context) |
| **GraphRAG** | Ground LLM answers in structured knowledge | [Concepts → GraphRAG](/concepts#graphrag) |
| **MCP Integration** | Use Semantica from Claude Desktop or VS Code | [MCP Server](/reference/mcp_server) |
</Step>
<Step title="Run the pipeline">
The full 6-step pipeline: ingest, parse, extract, build, visualize, export: is in the [Quickstart](quickstart). Takes under 5 minutes with pattern-based extraction (no API key required).
The full 6-step pipeline: ingest, parse, extract, build, visualize, export: is in the [Quickstart](/quickstart). Takes under 5 minutes with pattern-based extraction (no API key required).
<Note>
An LLM API key is **optional** for the quickstart. Pattern-based extraction works out of the box: upgrade to LLM extraction for higher accuracy when you're ready.
@@ -99,7 +99,7 @@ icon: "rocket"
print(f"{len(graph['entities'])} nodes, {len(graph['relationships'])} edges")
```
**Next:** [Full pipeline walkthrough →](quickstart)
**Next:** [Full pipeline walkthrough →](/quickstart)
</Tab>
<Tab title="Agent Context">
@@ -131,7 +131,7 @@ icon: "rocket"
precedents = context.find_precedents("model selection", limit=5)
```
**Next:** [Context module reference →](reference/context)
**Next:** [Context module reference →](/reference/context)
</Tab>
<Tab title="GraphRAG">
@@ -161,7 +161,7 @@ icon: "rocket"
print(f"{claim.text} → source: {claim.source_node}")
```
**Next:** [GraphRAG concepts →](concepts#graphrag)
**Next:** [GraphRAG concepts →](/concepts#graphrag)
</Tab>
<Tab title="MCP Integration">
@@ -183,9 +183,9 @@ icon: "rocket"
}
```
12 tools available instantly: extract entities, query graph, record decisions, run reasoning, export results.
15 tools available instantly: extract entities, query graph, record decisions, run reasoning, export results.
**Next:** [MCP Server reference →](reference/mcp_server)
**Next:** [MCP Server reference →](/reference/mcp_server)
</Tab>
</Tabs>
@@ -194,29 +194,29 @@ icon: "rocket"
Semantica uses a modular, layered architecture: import only what you need.
- **[Input Layer](reference/ingest)** — Load and prepare data from any source. Modules: `ingest`, `parse`, `split`, `normalize`
- **[Semantic Layer](reference/semantic_extract)** — Extract meaning from raw text. Modules: `semantic_extract`, `kg`, `ontology`, `reasoning`
- **[Storage Layer](reference/vector_store)** — Persist knowledge for retrieval. Modules: `embeddings`, `vector_store`, `graph_store`, `triplet_store`
- **[Quality Layer](reference/deduplication)** — Validate and deduplicate. Modules: `deduplication`, `conflicts`
- **[Context Layer](reference/context)** — Track decisions and lineage. Modules: `context`, `provenance`, `change_management`
- **[Output Layer](reference/export)** — Deliver results downstream. Modules: `export`, `visualization`, `pipeline`, `explorer`
- **[Input Layer](/reference/ingest)** — Load and prepare data from any source. Modules: `ingest`, `parse`, `split`, `normalize`
- **[Semantic Layer](/reference/semantic_extract)** — Extract meaning from raw text. Modules: `semantic_extract`, `kg`, `ontology`, `reasoning`
- **[Storage Layer](/reference/vector_store)** — Persist knowledge for retrieval. Modules: `embeddings`, `vector_store`, `graph_store`, `triplet_store`
- **[Quality Layer](/reference/deduplication)** — Validate and deduplicate. Modules: `deduplication`, `conflicts`
- **[Context Layer](/reference/context)** — Track decisions and lineage. Modules: `context`, `provenance`, `change_management`
- **[Output Layer](/reference/export)** — Deliver results downstream. Modules: `export`, `visualization`, `pipeline`, `explorer`
## Which Module Do I Need?
See the [Choose the Right Module](choose-your-module) guide — it maps 35+ developer goals to the right starting point across all 27 modules, with working code for the most common paths.
See the [Choose the Right Module](/choose-your-module) guide — it maps 35+ developer goals to the right starting point across all 27 modules, with working code for the most common paths.
## Next Steps
- [Core Concepts](concepts) — Knowledge graphs, ontologies, and reasoning explained in depth.
- [Quickstart Tutorial](quickstart) — Full 6-step pipeline walkthrough with working code.
- [Module Reference](modules) — Every module, class, and common chain explained.
- [API Reference](reference/context) — Complete module documentation for every class and method.
- [Core Concepts](/concepts) — Knowledge graphs, ontologies, and reasoning explained in depth.
- [Quickstart Tutorial](/quickstart) — Full 6-step pipeline walkthrough with working code.
- [Module Reference](/modules) — Every module, class, and common chain explained.
- [API Reference](/reference/context) — Complete module documentation for every class and method.
## Help
- [Discord](https://discord.gg/sV34vps5hH) — Ask questions, share projects, get community support.
- [GitHub Issues](https://github.com/semantica-agi/semantica/issues) — Report bugs or request features.
- [FAQ](faq) — Common questions answered.
- [FAQ](/faq) — Common questions answered.
+4 -4
View File
@@ -214,7 +214,7 @@ A vulnerability in XML parsers that allows attackers to read arbitrary files or
## See Also
- [Core Concepts](concepts) — Deeper explanation of key ideas with code examples.
- [Getting Started](getting-started) — First working examples: no prior graph experience required.
- [Modules Guide](modules) — All 27 modules explained with code and pipeline chains.
- [API Reference](reference/context) — Complete technical reference for every class and method.
- [Core Concepts](/concepts) — Deeper explanation of key ideas with code examples.
- [Getting Started](/getting-started) — First working examples: no prior graph experience required.
- [Modules Guide](/modules) — All 27 modules explained with code and pipeline chains.
- [API Reference](/reference/context) — Complete technical reference for every class and method.
+3 -3
View File
@@ -74,10 +74,10 @@ Semantica follows **Semantic Versioning** (`MAJOR.MINOR.PATCH`):
## License
MIT License: see [LICENSE](https://github.com/semantica-agi/semantica/blob/main/LICENSE) and the [License page](project-license).
MIT License: see [LICENSE](https://github.com/semantica-agi/semantica/blob/main/LICENSE) and the [License page](/project-license).
## See Also
- [Contributing](contributing-guide) — How to submit changes.
- [Community](community) — Community guidelines and channels.
- [Contributing](/contributing-guide) — How to submit changes.
- [Community](/community) — Community guidelines and channels.
+5 -5
View File
@@ -46,7 +46,7 @@ Agent Memory provides persistent storage and intelligent retrieval of informatio
- Simple retrieval tasks where relationships between entities don't matter
<Info>
This guide covers the memory layer. For graph-enriched traversal and entity linking, see [Context Graphs](context-graphs). For decision accountability — recording, auditing, and causally tracing what the agent chose — see [Decision Intelligence](decision-intelligence).
This guide covers the memory layer. For graph-enriched traversal and entity linking, see [Context Graphs](/guides/context-graphs). For decision accountability — recording, auditing, and causally tracing what the agent chose — see [Decision Intelligence](/guides/decision-intelligence).
</Info>
## Setting Up a Persistent Memory Context
@@ -657,10 +657,10 @@ print("Total memories: {}".format(s.get("total_items", 0)))
## Related Guides
- [Context Graphs](context-graphs) — How the underlying `ContextGraph` stores entity nodes and decision nodes; temporal interval reasoning; deduplication before node insertion; ontology from graph.
- [Decision Intelligence](decision-intelligence) — Recording decisions as graph nodes with causal chains and policy gating.
- [Multi-Agent Systems](multi-agent) — Coordinating multiple agents through a shared `AgentContext` and save/load handoffs.
- [LLM Integrations](llm-integrations) — Configuring the LLM provider passed to `query_with_reasoning()`.
- [Context Graphs](/guides/context-graphs) — How the underlying `ContextGraph` stores entity nodes and decision nodes; temporal interval reasoning; deduplication before node insertion; ontology from graph.
- [Decision Intelligence](/guides/decision-intelligence) — Recording decisions as graph nodes with causal chains and policy gating.
- [Multi-Agent Systems](/guides/multi-agent) — Coordinating multiple agents through a shared `AgentContext` and save/load handoffs.
- [LLM Integrations](/guides/llm-integrations) — Configuring the LLM provider passed to `query_with_reasoning()`.
- [Deduplication Guide](deduplication) — Full reference for `DuplicateDetector`, `EntityMerger`, similarity methods, and cluster strategies.
- [Ontology Management](ontology) — Generate and validate OWL ontologies from the knowledge graph; export to Turtle, OWL/XML, JSON-LD.
- [Context Module Reference](../reference/context) — Full API: `AgentContext`, `AgentMemory`, `MemoryItem`, `ContextRetriever`.
+2 -2
View File
@@ -496,8 +496,8 @@ print("Model v1.1 verified and approved for production.")
## Related Guides
- [Context Graphs](context-graphs) — `ContextGraph.to_dict()` feeds `create_snapshot()`
- [Context Graphs](/guides/context-graphs) — `ContextGraph.to_dict()` feeds `create_snapshot()`
- [Ontology Management](ontology) — pair ontology versioning with graph versioning for a complete schema + data audit trail
- [SHACL Validation](shacl-validation) — validate graph data at each version gate before snapshotting
- [SHACL Validation](/guides/shacl-validation) — validate graph data at each version gate before snapshotting
- [Provenance](provenance) — combine change management with W3C PROV-O lineage for a full audit trail
- [Visualization](visualization) — `TemporalVisualizer.visualize_snapshot_comparison()` and `visualize_metrics_evolution()` render version diffs as interactive charts
+3 -3
View File
@@ -69,7 +69,7 @@ flowchart TD
2. **Conflict Detection** — Call `detect_entity_conflicts()` to surface all property disagreements at once, or `detect_value_conflicts()` to target a specific property.
3. **Resolution** — For each conflict, apply a strategy (`CREDIBILITY_WEIGHTED`, `MOST_RECENT`, `VOTING`, etc.) or route it for expert review (`EXPERT_REVIEW`).
4. **Persist Canonical Values** — Write resolved values back to your canonical entities or graph store. See [Persisting resolved values](#persisting-resolved-values).
5. **SHACL Validation** — Enforce structural constraints on the resolved graph to confirm it satisfies your ontology. See [SHACL Validation](shacl-validation).
5. **SHACL Validation** — Enforce structural constraints on the resolved graph to confirm it satisfies your ontology. See [SHACL Validation](/guides/shacl-validation).
## Quick Start: A Beginner Example
@@ -698,6 +698,6 @@ Calling `set_resolution_rule()` for every entity-property pair just to apply the
- [Deduplication](deduplication) — remove duplicate nodes before running conflict detection
- [Provenance](provenance) — track which source each resolved value came from, and verify the audit trail cryptographically
- [SHACL Validation](shacl-validation) — enforce structural constraints after conflicts are resolved
- [Change Management](change-management) — snapshot the graph before and after conflict resolution runs
- [SHACL Validation](/guides/shacl-validation) — enforce structural constraints after conflicts are resolved
- [Change Management](/guides/change-management) — snapshot the graph before and after conflict resolution runs
- [Ontology Management](ontology) — align entity types to a shared vocabulary to reduce type conflicts at the schema level
+3 -3
View File
@@ -50,7 +50,7 @@ A context graph is a property graph that stores entities as **nodes** and relati
- Cases where setup complexity exceeds the relationship complexity
<Info>
ContextGraph is an **in-memory data structure**. All nodes, edges, and metadata are stored in Python dictionaries and lists. For standalone graphs, persist state with `save_to_file()`. When using `AgentContext`, call `AgentContext.save()` instead — it saves the graph, the FAISS vector index, and memory in one step. For analytical operations on top of a populated graph — centrality rankings, community detection, node embeddings, link prediction — see the [Graph Analytics guide](graph-analytics). For recording and querying decisions stored as nodes, see the [Decision Intelligence guide](decision-intelligence).
ContextGraph is an **in-memory data structure**. All nodes, edges, and metadata are stored in Python dictionaries and lists. For standalone graphs, persist state with `save_to_file()`. When using `AgentContext`, call `AgentContext.save()` instead — it saves the graph, the FAISS vector index, and memory in one step. For analytical operations on top of a populated graph — centrality rankings, community detection, node embeddings, link prediction — see the [Graph Analytics guide](/guides/graph-analytics). For recording and querying decisions stored as nodes, see the [Decision Intelligence guide](/guides/decision-intelligence).
</Info>
## Constructing the Graph
@@ -704,8 +704,8 @@ for n in stress_reach:
## Related Guides
- [Graph Analytics](graph-analytics) — centrality rankings, community detection, node embeddings, and link prediction on a populated `ContextGraph`
- [Decision Intelligence](decision-intelligence) — recording decisions as typed nodes, causal chain analysis, precedent search, and policy enforcement
- [Graph Analytics](/guides/graph-analytics) — centrality rankings, community detection, node embeddings, and link prediction on a populated `ContextGraph`
- [Decision Intelligence](/guides/decision-intelligence) — recording decisions as typed nodes, causal chain analysis, precedent search, and policy enforcement
- [Ingest](ingest) — loading data from PDFs, APIs, databases, STIX bundles, and RSS feeds into the graph
- [Deduplication](deduplication) — detecting and merging near-duplicate nodes before insertion to prevent graph fragmentation
- [Reasoning](reasoning) — temporal interval algebra (Allen relations), forward/backward chaining, and SPARQL over the knowledge graph
+4 -4
View File
@@ -638,8 +638,8 @@ results = context.find_precedents("APT29 infrastructure attribution", limit=5)
## Related Guides
- [Context Graphs](context-graphs) — how `ContextGraph` stores decision nodes and causal edges
- [Distance Intelligence](distance-intelligence) — `trace_decision_causality()` annotates causal chains with confidence decay and distance bands
- [Context Graphs](/guides/context-graphs) — how `ContextGraph` stores decision nodes and causal edges
- [Distance Intelligence](/guides/distance-intelligence) — `trace_decision_causality()` annotates causal chains with confidence decay and distance bands
- [Provenance](provenance) — W3C PROV-O audit trail that wraps decision records in standards-compliant provenance
- [MCP Server](mcp-server) — expose decision recording and precedent search to LLM agents via the `record_decision` and `find_precedents` tools
- [Change Management](change-management) — checkpoint decision state with `flush_checkpoint()` for versioned snapshots
- [MCP Server](/guides/mcp-server) — expose decision recording and precedent search to LLM agents via the `record_decision` and `find_precedents` tools
- [Change Management](/guides/change-management) — checkpoint decision state with `flush_checkpoint()` for versioned snapshots
+2 -2
View File
@@ -612,7 +612,7 @@ The similarity threshold controls sensitivity. Start at 0.7 and examine false po
## Related Guides
- [Ingest Anything](ingest) — multi-source ingestion creates the duplicates this module resolves
- [Context Graphs](context-graphs) — store deduplicated entities directly in the knowledge graph
- [Conflict Resolution](conflict-resolution) — after merging, reconcile disagreeing property values on the canonical entity
- [Context Graphs](/guides/context-graphs) — store deduplicated entities directly in the knowledge graph
- [Conflict Resolution](/guides/conflict-resolution) — after merging, reconcile disagreeing property values on the canonical entity
- [Provenance](provenance) — track merge lineage so every canonical entity traces back to its original sources
- [Pipeline](pipeline) — chain ingest, deduplicate, and store as a `PipelineBuilder` workflow
+4 -4
View File
@@ -557,8 +557,8 @@ for chain in chains:
## Related Guides
- [Context Graphs](context-graphs) — `ContextGraph` node and edge model; `add_edge(weight=...)` feeds confidence decay
- [Graph Analytics](graph-analytics) — centrality, community detection, Node2Vec embeddings, link prediction
- [Agent Memory](agent-memory) — proximity-blended retrieval (`proximity_weight`) integrates distance intelligence into memory search
- [Decision Intelligence](decision-intelligence) — `trace_decision_causality()` for causal chains with distance annotations
- [Context Graphs](/guides/context-graphs) — `ContextGraph` node and edge model; `add_edge(weight=...)` feeds confidence decay
- [Graph Analytics](/guides/graph-analytics) — centrality, community detection, Node2Vec embeddings, link prediction
- [Agent Memory](/guides/agent-memory) — proximity-blended retrieval (`proximity_weight`) integrates distance intelligence into memory search
- [Decision Intelligence](/guides/decision-intelligence) — `trace_decision_causality()` for causal chains with distance annotations
- [Reasoning & Rules](reasoning) — `TemporalReasoningEngine` for Allen interval algebra over time-bounded graph nodes
+2 -2
View File
@@ -443,8 +443,8 @@ For semantic reasoning and ontology work, OWL/XML is the format — it is the on
## Related Guides
- [Context Graphs](context-graphs) — the `ContextGraph` object whose `to_dict()` feeds all exports
- [Context Graphs](/guides/context-graphs) — the `ContextGraph` object whose `to_dict()` feeds all exports
- [Ontology Management](ontology) — export OWL ontologies generated from your graph
- [Reasoning & Rules](reasoning) — reasoning results can be exported as RDF triples
- [Change Management](change-management) — snapshot a graph before exporting to prove the export was made from a verified state
- [Change Management](/guides/change-management) — snapshot a graph before exporting to prove the export was made from a verified state
- [Pipeline](pipeline) — chain ingest, extract, and export in a single `PipelineBuilder`
+4 -4
View File
@@ -310,7 +310,7 @@ for node1, node2, score in predictions:
A score above 0.8 is worth analyst review — these aren't random; they're edges the topology of the existing graph strongly implies. Scores below 0.5 are noise. The sweet spot for human review is 0.60.8: plausible but not yet confirmed.
<Info>
Link prediction is also available on `Decision` nodes through `DecisionQuery.predict_decision_relationships(decision_id, top_k)`. See the [Decision Intelligence guide](decision-intelligence) for how to surface causal relationships between past decisions.
Link prediction is also available on `Decision` nodes through `DecisionQuery.predict_decision_relationships(decision_id, top_k)`. See the [Decision Intelligence guide](/guides/decision-intelligence) for how to surface causal relationships between past decisions.
</Info>
## Understanding Your Decision History
@@ -538,7 +538,7 @@ print(f"\n{len(result['communities'])} exposure clusters "
## Related Guides
- [Context Graphs](context-graphs) — building and querying the underlying `ContextGraph`
- [Context Graphs](/guides/context-graphs) — building and querying the underlying `ContextGraph`
- [Visualization](visualization) — render centrality rankings and community clusters as interactive dashboards
- [Decision Intelligence](decision-intelligence) — link prediction and structural similarity applied to decision nodes
- [GraphRAG](graphrag) — using analytics results to ground LLM generation in the most contextually relevant subgraph
- [Decision Intelligence](/guides/decision-intelligence) — link prediction and structural similarity applied to decision nodes
- [GraphRAG](/guides/graphrag) — using analytics results to ground LLM generation in the most contextually relevant subgraph
+5 -5
View File
@@ -576,9 +576,9 @@ The vector search and graph traversal run independently, then their scores are f
## Related Guides
- [Semantic Extraction](semantic-extraction) — build the graph from raw unstructured text
- [Agent Memory](agent-memory) — store, retrieve, and persist agent memories
- [Context Graphs](context-graphs) — build and traverse the knowledge graph directly
- [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](decision-intelligence) — causal chains, policy enforcement, decision tracking
- [LLM Integrations](llm-integrations) — connect Groq, OpenAI, Anthropic, HuggingFace, and 100+ more
- [Decision Intelligence](/guides/decision-intelligence) — causal chains, policy enforcement, decision tracking
- [LLM Integrations](/guides/llm-integrations) — connect Groq, OpenAI, Anthropic, HuggingFace, and 100+ more
+2 -2
View File
@@ -951,8 +951,8 @@ print(f"Compliance graph: {graph.stats()['node_count']} nodes, "
## Related Guides
- [Pipeline](pipeline) — chain ingest steps with `PipelineBuilder` for automated, retryable, parallelised workflows
- [Context Graphs](context-graphs) — storing and querying the entities you ingest as a typed property graph
- [Semantic Extraction](semantic-extraction) — NER, relation extraction, and triplet extraction from ingested text
- [Context Graphs](/guides/context-graphs) — storing and querying the entities you ingest as a typed property graph
- [Semantic Extraction](/guides/semantic-extraction) — NER, relation extraction, and triplet extraction from ingested text
- [Provenance](provenance) — tracking the origin document, confidence score, and ingestion timestamp for every extracted entity
- [Databricks Integration](../integrations/databricks) — Unity Catalog setup, PAT/OAuth M2M authentication, and lineage introspection
- [Snowflake Integration](../integrations/snowflake) — warehouse setup and password/key-pair/OAuth authentication
+4 -4
View File
@@ -719,7 +719,7 @@ for src in best["sources"]:
## Related Guides
- [Agent Memory](agent-memory) — using `query_with_reasoning()` with any LLM provider for graph-grounded retrieval
- [Multi-Agent Systems](multi-agent) — wiring different LLM providers to different agent tiers in a shared-graph pipeline
- [Semantic Extraction](semantic-extraction) — LLM-powered NER, relation extraction, event detection, and triplet extraction
- [GraphRAG](graphrag) — multi-hop graph reasoning with `query_with_reasoning()`
- [Agent Memory](/guides/agent-memory) — using `query_with_reasoning()` with any LLM provider for graph-grounded retrieval
- [Multi-Agent Systems](/guides/multi-agent) — wiring different LLM providers to different agent tiers in a shared-graph pipeline
- [Semantic Extraction](/guides/semantic-extraction) — LLM-powered NER, relation extraction, event detection, and triplet extraction
- [GraphRAG](/guides/graphrag) — multi-hop graph reasoning with `query_with_reasoning()`
+6 -4
View File
@@ -11,7 +11,7 @@ MCP stands for the Model Context Protocol. It is an open standard that allows ex
The Semantica MCP server exposes your knowledge graph as 12 callable tools. By connecting it, any compatible AI client can traverse the graph live, record decisions, run analytics, and export results during a conversation — without you having to write custom tool wrappers.
<Info>
The Semantica MCP server exposes 12 tools and 3 read-only resources. All tools accept and return JSON. No configuration beyond an optional environment variable for graph persistence is required.
The Semantica MCP server exposes 15 tools and 3 read-only resources. All tools accept and return JSON. No configuration beyond an optional environment variable for graph persistence is required.
</Info>
## Architecture & Communication
@@ -132,7 +132,7 @@ docker run --rm -i \
ghcr.io/semantica-agi/semantica-mcp:latest
```
## What the Agent Can Do: The 12 Tools
## What the Agent Can Do: The 15 Tools
Once connected, the LLM can call any of these tools during a conversation. The agent chains them automatically — you do not orchestrate the sequence, you just describe what you want.
@@ -140,6 +140,8 @@ Once connected, the LLM can call any of these tools during a conversation. The a
**Knowledge graph manipulation** — `add_entity` adds a node, `add_relationship` adds a directed edge. After extraction, the agent calls these to persist what it found into the live graph.
**Live graph queries and edits** — `query_graph` reads the graph without exporting it: fetch one node, walk its neighbours up to five hops, or keyword-search nodes. `update_node` merges properties onto an existing node (for example marking a task node `done`), and `delete_node` archives a node it no longer tracks. When `SEMANTICA_KG_PATH` is set, `update_node` and `delete_node` write their changes back to that file so they survive a restart.
**Decision intelligence** — `record_decision` writes a decision as a provenance node with confidence score, reasoning, and decision maker identity. `query_decisions` retrieves past decisions by query or category. `find_precedents` finds the most similar past decisions by semantic similarity. `get_causal_chain` traces decision causality upstream or downstream.
**Reasoning** — `run_reasoning` applies forward-chaining IF/THEN rules over a set of facts and returns derived conclusions.
@@ -341,7 +343,7 @@ The result is a fully auditable credit decision trail with precedent links, read
## Related Guides
- [Reasoning & Rules](reasoning) — the engine behind the `run_reasoning` tool
- [Decision Intelligence](decision-intelligence) — how decisions are stored as causal graph nodes
- [Context Graphs](context-graphs) — the graph that `add_entity` and `add_relationship` write to
- [Decision Intelligence](/guides/decision-intelligence) — how decisions are stored as causal graph nodes
- [Context Graphs](/guides/context-graphs) — the graph that `add_entity` and `add_relationship` write to
- [Export & Serialization](export) — all export formats available via `export_graph`
- [Ontology Management](ontology) — generate OWL ontologies from the graph built via MCP
+5 -5
View File
@@ -55,7 +55,7 @@ Semantica coordinates agents through shared context (memory and knowledge graphs
Semantica coordinates multiple agents through a shared `ContextGraph` — agents read and write to the same graph, or hand off serialized state via `save()` and `load()`, with no message broker required. Use this pattern when splitting work across ingestion, enrichment, reasoning, and reporting roles that must share a single evidence base.
<Info>
This guide covers multi-agent coordination. For the memory layer each agent uses internally, see [Agent Memory](agent-memory). For graph traversal and entity linking, see [Context Graphs](context-graphs). For decision recording and precedent matching, see [Decision Intelligence](decision-intelligence).
This guide covers multi-agent coordination. For the memory layer each agent uses internally, see [Agent Memory](/guides/agent-memory). For graph traversal and entity linking, see [Context Graphs](/guides/context-graphs). For decision recording and precedent matching, see [Decision Intelligence](/guides/decision-intelligence).
</Info>
## The Three Coordination Patterns
@@ -679,7 +679,7 @@ context.retrieve("...", user_id="analyst-jsmith")
## Related Guides
- [Agent Memory](agent-memory) — memory storage, retrieval, persistence, and the working memory window each agent uses internally
- [Context Graphs](context-graphs) — build and traverse the shared `ContextGraph` directly; temporal interval reasoning; entity deduplication before node insertion
- [Decision Intelligence](decision-intelligence) — record and trace decisions across agent handoffs with causal chain analysis
- [LLM Integrations](llm-integrations) — configure the LLM provider passed to `query_with_reasoning()` in each agent
- [Agent Memory](/guides/agent-memory) — memory storage, retrieval, persistence, and the working memory window each agent uses internally
- [Context Graphs](/guides/context-graphs) — build and traverse the shared `ContextGraph` directly; temporal interval reasoning; entity deduplication before node insertion
- [Decision Intelligence](/guides/decision-intelligence) — record and trace decisions across agent handoffs with causal chain analysis
- [LLM Integrations](/guides/llm-integrations) — configure the LLM provider passed to `query_with_reasoning()` in each agent
+4 -4
View File
@@ -297,7 +297,7 @@ export_rdf(ontology, "cyber_threat.jsonld", format="jsonld")
export_rdf(ontology, "cyber_threat.nt", format="ntriples")
```
The exported Turtle file is the input to Semantica's SHACL validation pipeline. See the [SHACL Validation](shacl-validation) guide for how to generate constraint shapes from this ontology and run them against live graph data.
The exported Turtle file is the input to Semantica's SHACL validation pipeline. See the [SHACL Validation](/guides/shacl-validation) guide for how to generate constraint shapes from this ontology and run them against live graph data.
---
@@ -503,8 +503,8 @@ else:
## Related Guides
- [SHACL Validation](shacl-validation) — generate W3C SHACL constraint shapes from your ontology and validate live graph data against them
- [SHACL Validation](/guides/shacl-validation) — generate W3C SHACL constraint shapes from your ontology and validate live graph data against them
- [Reasoning & Rules](reasoning) — apply forward/backward-chaining rules over your ontology to derive new facts
- [Export & Serialization](export) — export graphs to RDF, GraphML, CSV, and Neo4j Cypher
- [Semantic Extraction](semantic-extraction) — extract entities and relationships that feed ontology generation
- [Context Graphs](context-graphs) — the knowledge graph that ontology generation reads from
- [Semantic Extraction](/guides/semantic-extraction) — extract entities and relationships that feed ontology generation
- [Context Graphs](/guides/context-graphs) — the knowledge graph that ontology generation reads from
+2 -2
View File
@@ -717,6 +717,6 @@ print(f"Compliance delta update: {result.output}")
## Related Guides
- [Ingest](ingest) — all source types for the ingest step: PDFs, APIs, databases, RSS feeds, STIX directories, and streams
- [Semantic Extraction](semantic-extraction) — NER, relation extraction, triplet extraction, and event detection for the extract step
- [Context Graphs](context-graphs) — building and querying the `ContextGraph` that the store step populates
- [Semantic Extraction](/guides/semantic-extraction) — NER, relation extraction, triplet extraction, and event detection for the extract step
- [Context Graphs](/guides/context-graphs) — building and querying the `ContextGraph` that the store step populates
- [Provenance](provenance) — tracking the origin document, confidence score, and pipeline run ID for every extracted entity
+4 -4
View File
@@ -662,9 +662,9 @@ print("Policy updated to v2.4.0")
## Related Guides
- [Decision Intelligence](decision-intelligence) — `record_decision()`, causal chains, and precedent search — the decisions that `check_compliance()` evaluates
- [Decision Intelligence](/guides/decision-intelligence) — `record_decision()`, causal chains, and precedent search — the decisions that `check_compliance()` evaluates
- [Reasoning & Rules](reasoning) — complement policy rules with formal inference for logical conflict detection
- [SHACL Validation](shacl-validation) — enforce structural constraints on policy nodes themselves
- [Change Management](change-management) — version-snapshot the policy graph alongside the knowledge graph
- [SHACL Validation](/guides/shacl-validation) — enforce structural constraints on policy nodes themselves
- [Change Management](/guides/change-management) — version-snapshot the policy graph alongside the knowledge graph
- [Provenance](provenance) — W3C PROV-O lineage for every policy decision and exception
- [MCP Server](mcp-server) — expose `record_decision` and `find_precedents` as MCP tools for AI agents
- [MCP Server](/guides/mcp-server) — expose `record_decision` and `find_precedents` as MCP tools for AI agents
+2 -2
View File
@@ -659,7 +659,7 @@ Note: the banking example above passes `agent_id="credit_data_service_v2"` to `t
## Related Guides
- [Semantic Extraction](semantic-extraction) — the NER and relation extraction pipeline that auto-generates provenance entries for every extracted entity
- [Conflict Resolution](conflict-resolution) — provenance property sources feed directly into conflict detection; every resolved value is traceable to its source
- [Semantic Extraction](/guides/semantic-extraction) — the NER and relation extraction pipeline that auto-generates provenance entries for every extracted entity
- [Conflict Resolution](/guides/conflict-resolution) — provenance property sources feed directly into conflict detection; every resolved value is traceable to its source
- [Deduplication](deduplication) — merge operations are recorded in merge history; pair with provenance for a complete lineage from source to canonical entity
- [Provenance Reference](../reference/provenance) — full storage backend API, `InMemoryStorage`, `SQLiteStorage`, and `ProvenanceEntry` schema
+5 -5
View File
@@ -838,9 +838,9 @@ if proof:
## Related Guides
- [Semantic Extraction](semantic-extraction) — extract the entities and relationships that populate the graph facts you reason over
- [GraphRAG](graphrag) — retrieve graph-grounded context for LLM responses
- [Semantic Extraction](/guides/semantic-extraction) — extract the entities and relationships that populate the graph facts you reason over
- [GraphRAG](/guides/graphrag) — retrieve graph-grounded context for LLM responses
- [Ontology Management](ontology) — generate OWL ontologies to give your rules formal semantics
- [Decision Intelligence](decision-intelligence) — record and trace inferred decisions through the full causal chain
- [Context Graphs](context-graphs) — the knowledge graph that reasoning operates over
- [MCP Server](mcp-server) — expose `run_reasoning` as a tool for Claude and other agents
- [Decision Intelligence](/guides/decision-intelligence) — record and trace inferred decisions through the full causal chain
- [Context Graphs](/guides/context-graphs) — the knowledge graph that reasoning operates over
- [MCP Server](/guides/mcp-server) — expose `run_reasoning` as a tool for Claude and other agents
+4 -4
View File
@@ -71,7 +71,7 @@ This pipeline transforms documents like "APT29 deployed HAMMERTOSS malware targe
`semantica.semantic_extract` turns unstructured text into structured graph-ready output: it identifies named entities, extracts relationships between them, detects time-anchored events, resolves coreferences, and serialises everything as RDF triplets. Use it to populate a `ContextGraph` from raw documents — intelligence reports, clinical notes, regulatory filings, or any free-text corpus.
<Info>
Extracted entities and relationships feed into `ContextGraph` via `AgentContext.store()`. For how they are attributed back to source documents, see the [Provenance Guide](provenance). For how the populated graph is queried and traversed, see [Context Graphs](context-graphs).
Extracted entities and relationships feed into `ContextGraph` via `AgentContext.store()`. For how they are attributed back to source documents, see the [Provenance Guide](provenance). For how the populated graph is queried and traversed, see [Context Graphs](/guides/context-graphs).
</Info>
## Step 1 — Named Entity Recognition: who and what is in the text
@@ -664,8 +664,8 @@ The fallback behaviour is automatic: if the primary method returns an empty list
## Related Guides
- [Provenance Guide](provenance) — track every extracted entity and chunk back to its source document
- [Agent Memory Guide](agent-memory) — store extracted knowledge as searchable agent memories with graph enrichment
- [Context Graphs Guide](context-graphs) — how extracted entities populate `ContextGraph` nodes and edges
- [GraphRAG Guide](graphrag) — retrieve facts from the populated graph to ground LLM responses
- [Agent Memory Guide](/guides/agent-memory) — store extracted knowledge as searchable agent memories with graph enrichment
- [Context Graphs Guide](/guides/context-graphs) — how extracted entities populate `ContextGraph` nodes and edges
- [GraphRAG Guide](/guides/graphrag) — retrieve facts from the populated graph to ground LLM responses
- [Reasoning Guide](reasoning) — derive new facts, run SPARQL queries, and apply inference rules over the extracted graph
- [Semantic Extract Reference](../reference/semantic_extract) — full API for all extractor classes, providers, and validators
+2 -2
View File
@@ -740,5 +740,5 @@ def validate_before_publish(data_graph_str: str, ontology: dict) -> None:
- [Ontology Management](ontology) — generate the OWL ontology that SHACL shapes are derived from
- [Reasoning & Rules](reasoning) — complement SHACL structural constraints with logical inference rules
- [Export & Serialization](export) — serialize graph data to Turtle/RDF/XML for `run_shacl_validation` input
- [Conflict Resolution](conflict-resolution) — detect and resolve data conflicts before SHACL validation
- [Change Management](change-management) — version-gate SHACL shapes alongside ontology versions
- [Conflict Resolution](/guides/conflict-resolution) — detect and resolve data conflicts before SHACL validation
- [Change Management](/guides/change-management) — version-gate SHACL shapes alongside ontology versions
+3 -3
View File
@@ -614,8 +614,8 @@ fig.write_html("out.html") # manual export
## Related Guides
- [Context Graphs](context-graphs) — `graph.to_dict()` is the primary input for `KGVisualizer`
- [Context Graphs](/guides/context-graphs) — `graph.to_dict()` is the primary input for `KGVisualizer`
- [Ontology Management](ontology) — `OntologyVisualizer` renders ontologies produced by `OntologyGenerator`
- [Change Management](change-management) — `TemporalVersionManager` snapshots feed `visualize_metrics_evolution()` and `visualize_snapshot_comparison()`
- [Graph Analytics](graph-analytics) — centrality scores, community dicts, and connectivity results that feed the `AnalyticsVisualizer`
- [Change Management](/guides/change-management) — `TemporalVersionManager` snapshots feed `visualize_metrics_evolution()` and `visualize_snapshot_comparison()`
- [Graph Analytics](/guides/graph-analytics) — centrality scores, community dicts, and connectivity results that feed the `AnalyticsVisualizer`
- [Export & Serialization](export) — export the same graph to GraphML, GEXF, or DOT for Gephi and Graphviz
+14 -14
View File
@@ -185,8 +185,8 @@ decision_id = context.record_decision(
</CodeGroup>
- [Full Quickstart](quickstart) — Step-by-step pipeline walkthrough
- [Cookbook](cookbook) — 40+ real-world Jupyter notebooks
- [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
@@ -195,7 +195,7 @@ decision_id = context.record_decision(
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**
@@ -242,35 +242,35 @@ Semantica was designed for domains where every decision must be explainable and
```bash
pip install semantica
```
See [Installation](installation) for optional extras (`[all]`, `[neo4j]`, `[pinecone]`) and environment setup.
See [Installation](/installation) for optional extras (`[all]`, `[neo4j]`, `[pinecone]`) and environment setup.
</Step>
<Step title="Run the Quickstart">
Build a complete knowledge graph pipeline in [5 minutes](quickstart):
Build a complete knowledge graph pipeline in [5 minutes](/quickstart):
- Ingest documents from any source
- Extract entities and relationships
- Build and query the graph
- Record and trace a decision
</Step>
<Step title="Learn the mental model">
[Core Concepts](concepts) covers:
[Core Concepts](/concepts) covers:
- Knowledge graphs vs. vector stores: when to use each
- What GraphRAG is and how Semantica implements it
- How provenance and decision tracking work together
- The accountability layer architecture
</Step>
<Step title="Go deep on any module">
Every module has a dedicated [reference page](reference/context) with:
Every module has a dedicated [reference page](/reference/context) with:
- Full class and method documentation
- Parameter tables with types and defaults
- Runnable code examples for each feature
</Step>
</Steps>
- [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
- [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
@@ -369,7 +369,7 @@ Semantica was designed for domains where every decision must be explainable and
| `semantica.reasoning` | Forward chaining, Rete, deductive, abductive, SPARQL, Datalog |
| `semantica.ontology` | SHACL, SKOS, alignments, diff/migration, auto-generation, OWL/RDF |
| `semantica.explorer` | FastAPI Knowledge Explorer, Ontology Hub, Distance Intelligence, SHACL Studio |
| `semantica.mcp_server` | MCP stdio server: 12 tools for Claude Desktop, VS Code, Cursor, Windsurf, Cline |
| `semantica.mcp_server` | MCP stdio server: 15 tools for Claude Desktop, VS Code, Cursor, Windsurf, Cline |
| `semantica.vector_store` | FAISS, Pinecone, Weaviate, Qdrant, Milvus, PgVector |
| `semantica.graph_store` | Neo4j, FalkorDB, Apache AGE, Amazon Neptune |
| `semantica.triplet_store` | In-memory and persistent RDF triple store with SPARQL |
@@ -404,7 +404,7 @@ Semantica was designed for domains where every decision must be explainable and
- 1,000+ passing tests with full regression coverage
- `PipelineValidator` catches configuration errors at startup
- `FailureHandler` with exponential backoff and dead-letter queues
- 12 security vulnerabilities fixed in v0.5.0
- 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.
- Use `NERExtractor` without a graph store
+3 -3
View File
@@ -183,6 +183,6 @@ Install the [Microsoft Visual C++ Redistributable](https://aka.ms/vs/17/release/
## Next Steps
- [Getting Started](getting-started) — Understand what Semantica does before you build.
- [Build the Pipeline](quickstart) — Follow the end-to-end workflow with code.
- [Browse Examples](cookbook) — See notebook examples organized by use case.
- [Getting Started](/getting-started) — Understand what Semantica does before you build.
- [Build the Pipeline](/quickstart) — Follow the end-to-end workflow with code.
- [Browse Examples](/cookbook) — See notebook examples organized by use case.
+1 -1
View File
@@ -193,7 +193,7 @@ if not connector.test_connection():
## See Also
- [Ingest Module](../reference/ingest) — Full DatabricksIngestor and all other ingestors.
- [Snowflake Integration](snowflake) — Companion connector for a Snowflake + Databricks hybrid estate.
- [Snowflake Integration](/integrations/snowflake) — Companion connector for a Snowflake + Databricks hybrid estate.
- [Pipeline](../reference/pipeline) — Use Databricks ingestion as a pipeline step.
- [Installation](../installation) — All optional dependency extras.
- [Knowledge Graph](../reference/kg) — Build a KG from ingested Databricks data.
+2 -2
View File
@@ -370,7 +370,7 @@ Common causes of authentication failures:
## See Also
- [Ingest Module](../reference/ingest) — Full `SalesforceIngestor` API and all other ingestors.
- [Snowflake Integration](snowflake) — Relational warehouse connector with a similar design.
- [Databricks Integration](databricks) — Lakehouse connector.
- [Snowflake Integration](/integrations/snowflake) — Relational warehouse connector with a similar design.
- [Databricks Integration](/integrations/databricks) — Lakehouse connector.
- [Installation](../installation) — All optional dependency extras.
- [Knowledge Graph](../reference/kg) — Build a KG from ingested Salesforce data.
+1 -1
View File
@@ -172,7 +172,7 @@ if not connector.test_connection():
## See Also
- [Ingest Module](../reference/ingest) — Full SnowflakeIngestor and all other ingestors.
- [Databricks Integration](databricks) — Companion connector for a Snowflake + Databricks hybrid estate.
- [Databricks Integration](/integrations/databricks) — Companion connector for a Snowflake + Databricks hybrid estate.
- [Pipeline](../reference/pipeline) — Use Snowflake ingestion as a pipeline step.
- [Installation](../installation) — All optional dependency extras.
- [Knowledge Graph](../reference/kg) — Build a KG from ingested Snowflake data.
+13 -13
View File
@@ -9,9 +9,9 @@ Whether you're running your first pipeline or deploying Semantica in production,
## Learning Paths
- **Beginner (12 hrs)** — New to Semantica and knowledge graphs. [Start with Installation →](installation)
- **Intermediate (46 hrs)** — Comfortable with basics, building real applications. [Start with Modules →](modules)
- **Advanced (8+ hrs)** — Enterprise deployments, customization, and extension. [Start with Architecture →](architecture)
- **Beginner (12 hrs)** — New to Semantica and knowledge graphs. [Start with Installation →](/installation)
- **Intermediate (46 hrs)** — Comfortable with basics, building real applications. [Start with Modules →](/modules)
- **Advanced (8+ hrs)** — Enterprise deployments, customization, and extension. [Start with Architecture →](/architecture)
<Tabs>
<Tab title="Beginner (12 hrs)">
@@ -19,16 +19,16 @@ Whether you're running your first pipeline or deploying Semantica in production,
<Steps>
<Step title="Set up your environment">
[Installation Guide](installation): virtual environments, optional extras, platform-specific fixes.
[Installation Guide](/installation): virtual environments, optional extras, platform-specific fixes.
</Step>
<Step title="Understand the core ideas">
[Core Concepts](concepts): what knowledge graphs are, how embeddings work, what extraction does.
[Core Concepts](/concepts): what knowledge graphs are, how embeddings work, what extraction does.
</Step>
<Step title="Run your first example">
[Getting Started](getting-started): 5-minute code walkthrough with pattern-based extraction (no API key needed).
[Getting Started](/getting-started): 5-minute code walkthrough with pattern-based extraction (no API key needed).
</Step>
<Step title="Build your first knowledge graph">
[Quickstart Tutorial](quickstart): full 6-step pipeline from ingestion to visualization.
[Quickstart Tutorial](/quickstart): full 6-step pipeline from ingestion to visualization.
</Step>
<Step title="Explore interactively">
[Welcome to Semantica notebook](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb): Jupyter walkthrough of every module.
@@ -40,13 +40,13 @@ Whether you're running your first pipeline or deploying Semantica in production,
<Steps>
<Step title="Learn every module">
[Modules Guide](modules): all 27 modules with code examples and common pipeline chains.
[Modules Guide](/modules): all 27 modules with code examples and common pipeline chains.
</Step>
<Step title="Build production knowledge graphs">
[Building Knowledge Graphs notebook](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/07_Building_Knowledge_Graphs.ipynb): multi-source, deduplication, conflict resolution.
</Step>
<Step title="Add semantic search">
[Embeddings notebook](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/09_Embeddings.ipynb): providers, pooling strategies, vector stores.
[Embedding Generation notebook](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/12_Embedding_Generation.ipynb): generating embeddings, provider and model switching, dimensions. Then [Vector Store notebook](https://github.com/semantica-agi/semantica/blob/main/cookbook/introduction/13_Vector_Store.ipynb): storing and searching vectors for retrieval.
</Step>
<Step title="Multi-source integration">
[Multi-Source Data Integration notebook](https://github.com/semantica-agi/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb) for multi-source patterns.
@@ -58,7 +58,7 @@ Whether you're running your first pipeline or deploying Semantica in production,
<Steps>
<Step title="Understand the architecture">
[Architecture Guide](architecture): four-layer design, extension points, and design decisions.
[Architecture Guide](/architecture): four-layer design, extension points, and design decisions.
</Step>
<Step title="Temporal intelligence">
[Temporal Graphs notebook](https://github.com/semantica-agi/semantica/blob/main/cookbook/advanced/10_Temporal_Knowledge_Graphs.ipynb): `valid_from`/`valid_until`, Allen interval algebra, point-in-time queries.
@@ -236,6 +236,6 @@ The `blocking_v2`, `hybrid_v2`, and `semantic_v2` strategies reduce O(n²) compa
- **Graph exports**: encrypt sensitive exports at rest; use the v0.5.0 SSRF-safe `base_url` validation when configuring custom LLM gateways
- **XML ingestion**: always use `XMLIngestor` (v0.5.0), which uses the XXE-safe lxml backend; never parse untrusted XML with the standard library parser
- [Cookbook](cookbook) — Interactive Jupyter notebooks from beginner to advanced.
- [FAQ](faq) — Common questions answered.
- [API Reference](reference/core) — Complete technical documentation.
- [Cookbook](/cookbook) — Interactive Jupyter notebooks from beginner to advanced.
- [FAQ](/faq) — Common questions answered.
- [API Reference](/reference/core) — Complete technical documentation.
+32 -32
View File
@@ -9,7 +9,7 @@ icon: "puzzle-piece"
</Info>
<Tip>
Not sure which module to use? The [Choose the Right Module](choose-your-module) guide maps 35+ developer goals to modules with code examples — start there if you're orienting for the first time.
Not sure which module to use? The [Choose the Right Module](/choose-your-module) guide maps 35+ developer goals to modules with code examples — start there if you're orienting for the first time.
</Tip>
Semantica is organized into **27 modules** across six logical layers. Each module is independently importable: you never pay for what you don't use.
@@ -438,7 +438,7 @@ Exposes Semantica as an MCP stdio server for IDE and agent integrations.
python -m semantica.mcp_server
```
**Integrations:** Claude Desktop, VS Code, Cursor, Windsurf, Cline: 12 MCP tools exposed
**Integrations:** Claude Desktop, VS Code, Cursor, Windsurf, Cline: 15 MCP tools exposed
### Seed
@@ -680,34 +680,34 @@ versioner.create_snapshot(kg, "2024-Q1", author="user@example.com", description=
| Module | Purpose | Key Classes |
| :------ | :------- | :----------- |
| [ingest](reference/ingest) | Data ingestion | `FileIngestor`, `WebIngestor`, `ParquetIngestor`, `XMLIngestor` |
| [parse](reference/parse) | Document parsing | `DocumentParser`, `DoclingParser` |
| [split](reference/split) | Text chunking | `TextSplitter` |
| [normalize](reference/normalize) | Data cleaning | `TextNormalizer`, `EntityNormalizer`, `LanguageDetector` |
| [semantic_extract](reference/semantic_extract) | NER & relation extraction | `NERExtractor`, `RelationExtractor`, `TripletExtractor`, `SemanticAnalyzer`, `SemanticNetworkExtractor`, `ExtractionValidator` |
| [kg](reference/kg) | Graph construction | `GraphBuilder`, `TemporalGraphQuery`, `SimilarityCalculator` |
| [ontology](reference/ontology) | Schema management | `OntologyGenerator`, `SHACLGenerator` |
| [reasoning](reference/reasoning) | Logical inference | `Reasoner`, `DatalogReasoner` |
| [embeddings](reference/embeddings) | Vector embeddings | `EmbeddingGenerator` |
| [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 | `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` |
| [export](reference/export) | Data export | `RDFExporter`, `ParquetExporter` |
| [visualization](reference/visualization) | Graph visualization | `KGVisualizer` |
| [pipeline](reference/pipeline) | Workflow orchestration | `Pipeline`, `PipelineBuilder` |
| [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 | `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` |
| [ingest](/reference/ingest) | Data ingestion | `FileIngestor`, `WebIngestor`, `ParquetIngestor`, `XMLIngestor` |
| [parse](/reference/parse) | Document parsing | `DocumentParser`, `DoclingParser` |
| [split](/reference/split) | Text chunking | `TextSplitter` |
| [normalize](/reference/normalize) | Data cleaning | `TextNormalizer`, `EntityNormalizer`, `LanguageDetector` |
| [semantic_extract](/reference/semantic_extract) | NER & relation extraction | `NERExtractor`, `RelationExtractor`, `TripletExtractor`, `SemanticAnalyzer`, `SemanticNetworkExtractor`, `ExtractionValidator` |
| [kg](/reference/kg) | Graph construction | `GraphBuilder`, `TemporalGraphQuery`, `SimilarityCalculator` |
| [ontology](/reference/ontology) | Schema management | `OntologyGenerator`, `SHACLGenerator` |
| [reasoning](/reference/reasoning) | Logical inference | `Reasoner`, `DatalogReasoner` |
| [embeddings](/reference/embeddings) | Vector embeddings | `EmbeddingGenerator` |
| [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 | `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` |
| [export](/reference/export) | Data export | `RDFExporter`, `ParquetExporter` |
| [visualization](/reference/visualization) | Graph visualization | `KGVisualizer` |
| [pipeline](/reference/pipeline) | Workflow orchestration | `Pipeline`, `PipelineBuilder` |
| [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 | `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` |
- [Getting Started](getting-started) — Your first knowledge graph in 5 minutes.
- [Cookbook](cookbook) — 40+ domain notebooks with real-world examples.
- [API Reference](reference/context) — Full technical documentation.
- [Getting Started](/getting-started) — Your first knowledge graph in 5 minutes.
- [Cookbook](/cookbook) — 40+ domain notebooks with real-world examples.
- [API Reference](/reference/context) — Full technical documentation.
+2 -2
View File
@@ -76,5 +76,5 @@ By contributing to Semantica, you agree that your contributions will be licensed
## See Also
- [Contributing](contributing-guide) — How to contribute to the project.
- [Citation](citation) — How to cite Semantica in research.
- [Contributing](/contributing-guide) — How to contribute to the project.
- [Citation](/citation) — How to cite Semantica in research.
+89 -66
View File
@@ -5,7 +5,7 @@ icon: "rocket"
---
<Info>
**v0.5.0**Ontology Hub, Distance Intelligence, Parquet & XML ingestion, 12 security fixes. <a href="https://github.com/semantica-agi/semantica/releases" style={{color:"#10B981",fontWeight:600,textDecoration:"none"}}>What's new →</a>
**v0.6.7**first-class LangChain integration, SAP OData ingestor, human-editable Markdown persistence for `ContextGraph`, and a structured Action layer for the reasoning engine. <a href="https://github.com/semantica-agi/semantica/releases" style={{color:"#10B981",fontWeight:600,textDecoration:"none"}}>What's new →</a>
</Info>
This guide walks you through the end-to-end pipeline for building your first knowledge graph. Start here after installation. An LLM API key is optional: pattern-based extraction works out of the box.
@@ -35,7 +35,7 @@ Verify:
```bash
python -c "import semantica; print(semantica.__version__)"
# 0.5.0
# 0.6.7
```
@@ -47,36 +47,24 @@ python -c "import semantica; print(semantica.__version__)"
<Step title="Ingest">
Load a document from a file, directory, URL, or database.
Load a document from a file or directory. The rest of this walkthrough follows
the file path; other sources are shown afterwards.
<CodeGroup>
```python File
```python
from semantica.ingest import FileIngestor
ingestor = FileIngestor()
sources = ingestor.ingest("data/report.pdf")
# Also accepts: .docx, .html, .json, .csv, .xlsx, .pptx, .parquet, .xml
# Also accepts a directory, .docx, .html, .json, .csv, .xlsx, .pptx, .parquet, .xml
```
```python Web
from semantica.ingest import WebIngestor
ingestor = WebIngestor(max_depth=2)
sources = ingestor.ingest("https://example.com/article")
```
```python Parquet / XML (v0.5.0)
from semantica.ingest import ParquetIngestor, XMLIngestor
# Single file or Hive-partitioned directory
sources = ParquetIngestor().ingest("data/events.parquet")
# XML with XSD schema validation
sources = XMLIngestor(validate_xsd="schema.xsd").ingest("data/records/")
```
</CodeGroup>
<Tip>
**Other sources.** `WebIngestor().ingest_url(url)` returns a `WebContent` whose
`.text` you can feed straight into the Extract step (no parsing needed).
`ParquetIngestor().ingest(path)` and `XMLIngestor().ingest(path, schema_path=...)`
return structured records rather than documents; build a graph from those with
`GraphBuilder().build({"entities": [...], "relationships": [...]})` directly.
</Tip>
</Step>
@@ -88,22 +76,24 @@ Extract structured text and layout from raw documents.
from semantica.parse import DocumentParser
parser = DocumentParser()
parsed = parser.parse(sources[0])
parsed = parser.parse(sources[0].path) # parse() takes a path string
print(parsed.text[:200]) # extracted text
print(parsed.metadata) # title, author, date, source
print(parsed["text"][:200]) # extracted text
print(parsed["metadata"]) # file_path, encoding, size, and format-specific keys
```
`parse()` returns a `dict` with `text`, `full_text`, and `metadata` keys.
<Tip>
For PDFs with tables, charts, or multi-column layouts, use `DoclingParser`: it applies advanced layout analysis and returns structured table data alongside text.
For PDFs with tables, charts, or multi-column layouts, use `DoclingParser` (`pip install semantica[parse-docling]`): it applies advanced layout analysis and returns structured table data alongside text.
</Tip>
```python
from semantica.parse import DoclingParser
parser = DoclingParser()
parsed = parser.parse(sources[0])
print(parsed.tables) # structured table objects
parsed = parser.parse(sources[0].path)
print(parsed["tables"]) # structured table data
```
</Step>
@@ -117,26 +107,28 @@ Identify named entities and extract typed relationships between them.
```python Pattern-based (fast, no API key)
from semantica.semantic_extract import NERExtractor, RelationExtractor
ner = NERExtractor(method="pattern")
entities = ner.extract(parsed)
# Returns: [{"text": "Apple Inc.", "type": "ORGANIZATION", "confidence": 0.98}, ...]
text = parsed["text"]
rel = RelationExtractor(method="rule")
relationships = rel.extract(parsed, entities=entities)
# Returns: [{"subject": "Steve Jobs", "predicate": "founded", "object": "Apple Inc."}, ...]
ner = NERExtractor(method="pattern")
entities = ner.extract(text)
# Returns: [Entity(text="Apple Inc.", label="ORG", start_char=0, end_char=10, confidence=0.7), ...]
rel = RelationExtractor(method="pattern")
relationships = rel.extract(text, entities=entities)
# Returns: [Relation(subject=Entity(...), predicate="founded_by", object=Entity(...), confidence=0.7), ...]
```
```python LLM-powered (higher accuracy)
from semantica.semantic_extract import NERExtractor, RelationExtractor
from semantica.llms import Groq
llm = Groq(model="llama-3.3-70b-versatile")
# Reads GROQ_API_KEY from the environment; provider/llm_model select the backend
text = parsed["text"]
ner = NERExtractor(method="llm", llm_provider=llm)
entities = ner.extract(parsed)
ner = NERExtractor(method="llm", provider="groq", llm_model="llama-3.3-70b-versatile")
entities = ner.extract(text)
rel = RelationExtractor(method="llm", llm_provider=llm)
relationships = rel.extract(parsed, entities=entities)
rel = RelationExtractor(method="llm", provider="groq", llm_model="llama-3.3-70b-versatile")
relationships = rel.extract(text, entities=entities)
```
</CodeGroup>
@@ -198,16 +190,17 @@ exporter.export(graph, file_path="graph.nt", format="nt")
from semantica.export import ParquetExporter
exporter = ParquetExporter()
exporter.export(graph, file_path="output/graph.parquet")
# Writes nodes.parquet + edges.parquet: ready for Spark, BigQuery, Databricks
exporter.export(graph, file_path="output/graph")
# Dict input writes one file per key: output/graph_entities.parquet and
# output/graph_relationships.parquet: ready for Spark, BigQuery, Databricks
```
```python ArangoDB
from semantica.export import ArangoAQLExporter
exporter = ArangoAQLExporter()
aql = exporter.export(graph)
# Returns ready-to-run AQL INSERT statements
exporter.export(graph, file_path="graph.aql")
# Writes ready-to-run AQL INSERT statements to graph.aql
```
</CodeGroup>
@@ -272,14 +265,21 @@ relationships = rel.extract(text, entities=entities)
<Accordion title="Multi-source incremental graph build" icon="layer-group">
```python
from semantica.ingest import FileIngestor
from semantica.parse import DocumentParser
from semantica.semantic_extract import NERExtractor, RelationExtractor
from semantica.kg import GraphBuilder
builder = GraphBuilder(merge_entities=True)
all_entities, all_rels = [], []
parser = DocumentParser()
ner = NERExtractor(method="pattern")
rel = RelationExtractor(method="pattern")
builder = GraphBuilder(merge_entities=True)
for doc in parsed_docs:
entities = ner.extract(doc)
rels = rel.extract(doc, entities=entities)
all_entities, all_rels = [], []
for source in FileIngestor().ingest("data/reports/"):
text = parser.parse(source.path)["text"]
entities = ner.extract(text)
rels = rel.extract(text, entities=entities)
all_entities.extend(entities)
all_rels.extend(rels)
@@ -359,7 +359,8 @@ graph = builder.build({"entities": entities, "relationships": relationships})
# Retrieve full lineage for any entity
sources = prov.get_all_sources("Apple Inc.")
print(sources[0])
# {"source": "data/report.pdf", "location": None, "timestamp": "...", "confidence": 0.98}
# {"source": "data/report.pdf", "location": None, "timestamp": "...",
# "confidence": 1.0, "metadata": {"confidence": 0.98}}
```
</Accordion>
@@ -373,32 +374,54 @@ print(sources[0])
<Accordion title="No entities extracted" icon="magnifying-glass">
The document likely contains scanned images rather than machine-readable text. Enable OCR:
The document likely contains scanned images rather than machine-readable text. `DocumentParser` warns when a PDF has no text layer; switch to `DoclingParser` with OCR enabled:
```python
from semantica.parse import DocumentParser
from semantica.parse import DoclingParser # pip install semantica[parse-docling]
parser = DocumentParser(ocr=True) # enables Tesseract OCR
parsed = parser.parse(sources[0])
parser = DoclingParser(enable_ocr=True)
parsed = parser.parse(sources[0].path)
```
</Accordion>
<Accordion title="Slow processing on large corpora" icon="gauge">
Enable parallel processing and GPU acceleration:
Install the GPU extras so embedding and ML inference run on CUDA:
```bash
pip install semantica[gpu]
```
```python
from semantica.pipeline import Pipeline
Scan the directory for paths first (no file contents are read), then handle one
document at a time and write to a persistent graph backend instead of the
in-memory graph:
pipeline = Pipeline(workers=8, batch_size=32)
pipeline.run(sources)
```python
from semantica.ingest import FileIngestor
from semantica.parse import DocumentParser
from semantica.semantic_extract import NERExtractor, RelationExtractor
from semantica.graph_store import GraphStore
from semantica.kg import GraphBuilder
ingestor = FileIngestor()
parser = DocumentParser()
ner = NERExtractor(method="pattern")
rel = RelationExtractor(method="pattern")
store = GraphStore(backend="neo4j", uri="bolt://localhost:7687",
user="neo4j", password="password")
builder = GraphBuilder(merge_entities=True, graph_store=store)
for info in ingestor.scan_directory("data/reports/", recursive=True):
text = parser.parse(info["path"])["text"] # one document loaded at a time
entities = ner.extract(text)
rels = rel.extract(text, entities=entities)
builder.build({"entities": entities, "relationships": rels})
```
For multi-step orchestration with configurable parallelism, see the
[Pipeline guide](/guides/pipeline).
</Accordion>
<Accordion title="Memory errors on large graphs" icon="memory">
@@ -429,7 +452,7 @@ pip install --upgrade semantica
## Next Steps
- [Core Concepts](concepts) — Knowledge graphs, ontologies, reasoning engines: the mental model behind Semantica.
- [Module Reference](modules) — Every module explained with key classes and common chains.
- [API Reference](reference/context) — Complete documentation for every module, class, and parameter.
- [Cookbook](cookbook) — 40+ interactive Jupyter notebooks with real-world datasets.
- [Core Concepts](/concepts) — Knowledge graphs, ontologies, reasoning engines: the mental model behind Semantica.
- [Module Reference](/modules) — Every module explained with key classes and common chains.
- [API Reference](/reference/context) — Complete documentation for every module, class, and parameter.
- [Cookbook](/cookbook) — 40+ interactive Jupyter notebooks with real-world datasets.
+2 -2
View File
@@ -351,6 +351,6 @@ for record in history:
</AccordionGroup>
- [Provenance](provenance) — W3C PROV-O lineage tracking.
- [Knowledge Graph](kg) — The graph being versioned.
- [Knowledge Graph](/reference/kg) — The graph being versioned.
- [Export](export) — Export versioned snapshots.
- [Conflicts](conflicts) — Detect conflicts introduced between versions.
- [Conflicts](/reference/conflicts) — Detect conflicts introduced between versions.
+1 -1
View File
@@ -453,4 +453,4 @@ class InvestigationStep:
- [Deduplication](deduplication) — Resolve duplicate entities before conflict detection.
- [Ontology](ontology) — Logical conflicts use SHACL shapes and ontology axioms.
- [Provenance](provenance) — Track which source each conflicting fact came from.
- [Knowledge Graph](kg) — The graph being checked for conflicts.
- [Knowledge Graph](/reference/kg) — The graph being checked for conflicts.
+3 -3
View File
@@ -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](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
@@ -1087,8 +1087,8 @@ class EntityLink:
</Tab>
</Tabs>
- [Vector Store](vector_store) — Embedding storage backend for memory retrieval.
- [Knowledge Graph](kg) — Graph algorithms and analytics used inside ContextGraph.
- [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.
+2 -2
View File
@@ -227,6 +227,6 @@ result = build_knowledge_base(sources=["doc.pdf"], method="fast")
</Tip>
- [Pipeline](pipeline) — Pipeline execution and step orchestration.
- [Utils](utils) — Shared utilities used by Core internally.
- [Utils](/reference/utils) — Shared utilities used by Core internally.
- [Getting Started](../getting-started) — Learn the basics before using Core.
- [LLMs](llms) — Configure LLM providers via ConfigManager.
- [LLMs](/reference/llms) — Configure LLM providers via ConfigManager.
+3 -3
View File
@@ -437,7 +437,7 @@ result = calculate_similarity(entity_a, entity_b, method="drug_name")
</Tab>
</Tabs>
- [Conflicts](conflicts) — Detect value conflicts between non-duplicate entities.
- [Knowledge Graph](kg) — GraphBuilder uses deduplication during construction.
- [Normalize](normalize) — Normalize entity names before deduplication.
- [Conflicts](/reference/conflicts) — Detect value conflicts between non-duplicate entities.
- [Knowledge Graph](/reference/kg) — GraphBuilder uses deduplication during construction.
- [Normalize](/reference/normalize) — Normalize entity names before deduplication.
- [Provenance](provenance) — Track merged entity lineage.
+3 -5
View File
@@ -607,9 +607,7 @@ The Knowledge Explorer embeds Distance Intelligence directly in the browser dash
The 10× cache improvement applies when the graph is unchanged between requests. In write-heavy pipelines where nodes are added continuously, cache hit rates will be lower. Use `force_refresh=False` (default) for read-heavy Explorer usage and `force_refresh=True` for batch pipeline contexts.
</Note>
- [Context Module](context) — `ContextGraph.get_neighbors()` and proximity-blended retrieval.
- [Knowledge Graph Module](kg) — `NodeEmbedder`, `SimilarityCalculator`, and graph analytics.
- [Context Module](/reference/context) — `ContextGraph.get_neighbors()` and proximity-blended retrieval.
- [Knowledge Graph Module](/reference/kg) — `NodeEmbedder`, `SimilarityCalculator`, and graph analytics.
- [Visualization](visualization) — Programmatic distance heatmaps and ego-mode graph renders.
- [Explorer](explorer) — Knowledge Explorer with built-in Distance Intelligence dashboard.
- [Distance Intelligence](https://github.com/semantica-agi/semantica/blob/main/cookbook/advanced/12_Distance_Intelligence.ipynb) — Semantic neighborhoods and distance matrices · Advanced
- [Explorer](/reference/explorer) — Knowledge Explorer with built-in Distance Intelligence dashboard.
+3 -3
View File
@@ -619,7 +619,7 @@ providers = check_available_providers()
# → {"sentence_transformers": True, "fastembed": True, "openai": False}
```
- [Vector Store](vector_store) — Store and search the generated embeddings.
- [Split](split) — Chunk text before embedding for better retrieval quality.
- [KG Module](kg) — Distance Intelligence uses graph embeddings for semantic neighbourhoods.
- [Vector Store](/reference/vector_store) — Store and search the generated embeddings.
- [Split](/reference/split) — Chunk text before embedding for better retrieval quality.
- [KG Module](/reference/kg) — Distance Intelligence uses graph embeddings for semantic neighbourhoods.
- [Deduplication](deduplication) — Semantic deduplication uses embedding distance for entity resolution.
+209 -49
View File
@@ -1,64 +1,224 @@
---
title: "Evals Module"
description: "Evaluation framework for measuring Knowledge Graph quality, extraction accuracy, and pipeline performance: coming soon."
description: "Score decision records, audit trails, and reasoning output with deterministic and model-backed evaluators plus a small run harness."
icon: "chart-line"
---
**`semantica.evals`** is planned as a comprehensive evaluation framework for measuring **extraction accuracy, graph quality, and pipeline performance**.
`semantica.evals` measures the quality of decision intelligence outputs. It takes
the decisions, audit trails, and reasoning text your pipeline produces and scores
them against expectations you define, returning a structured summary you can log,
assert on in tests, or track across runs.
<Warning>
**`semantica.evals` is not yet implemented.** The module is a placeholder with `__all__ = []`. No classes or functions are available for import. This page describes the planned API only.
</Warning>
- A registry of named evaluators, from exact string matching to ROUGE overlap and
LLM-as-judge
- `decision_scores`, a composite evaluator for `Decision` objects that checks
outcome, confidence bounds, required fields, provenance, and (optionally)
policy compliance
- A `evaluate()` runner that applies several evaluators to a list of cases and
aggregates pass / fail / error counts
- Per-evaluator **objectives** that let you override an evaluator's built-in
verdict at the run level
## Planned Features
<Note>
The module is versioned separately from the package: `semantica.evals.__version__`
is `"0.1.0"`. The public surface described here is stable, but expect additive
changes (new evaluators, new objective options) before it reaches 1.0.
</Note>
When released, `semantica.evals` will provide:
## Public API
| Planned Class | Role |
| :--- | :--- |
| `KGEvaluator` | Completeness, consistency, schema compliance, coverage, and orphan node detection |
| `ExtractionEvaluator` | NER precision / recall / F1 and relation extraction metrics against gold datasets |
| `PipelineBenchmark` | Throughput (docs/sec), per-step latency, peak memory, and error rate |
| `RegressionTracker` | Record runs and compare metrics across commits or config changes |
| `EvalReport` | Structured report: `{scores, regressions, recommendations}` |
| `DeduplicationEvaluator` | Merge precision, false positive / false negative rates |
| `ReasoningEvaluator` | Inference accuracy, rule coverage, and derivation depth |
## Current Workaround
Until `semantica.evals` ships, use `semantica.ontology.OntologyEvaluator` for ontology quality metrics:
| Name | Kind | Role |
| :--- | :--- | :--- |
| `evaluate(cases, evaluators, config=None, target_fn=None)` | function | Run named evaluators over each case, return an `EvalSummary` |
| `list_evaluators()` | function | Sorted names of every registered evaluator |
| `get_evaluator(name)` | function | Look up a single evaluator function by name |
| `EvalMetric` | dataclass (frozen) | One evaluator's result: `score`, `passed`, `meta` |
| `CaseResult` | namedtuple | One case's result: `case_id`, `status`, `metrics`, `details` |
| `EvalSummary` | dataclass | Aggregate across cases: `total`, `passed`, `failed`, `errors`, `pass_rate`, `cases` |
```python
from semantica.ontology import OntologyEvaluator
evaluator = OntologyEvaluator()
# evaluate_ontology takes the ontology dict only
result = evaluator.evaluate_ontology(ontology)
print("Coverage: ", result.coverage_score)
print("Completeness:", result.completeness_score)
print("Gaps: ", result.gaps)
print("Suggestions: ", result.suggestions)
# Full report with class granularity and relation completeness
report = evaluator.generate_report(ontology)
print("Coverage score: ", report["evaluation"]["coverage_score"])
print("Completeness score:", report["evaluation"]["completeness_score"])
print("Relation coverage: ", report["relation_completeness"]["relation_coverage"])
import semantica.evals as evals
from semantica.evals import evaluate, list_evaluators, get_evaluator
```
`EvaluationResult` fields returned by `evaluate_ontology()`:
## Built-in evaluators
| Field | Type | Description |
| :----- | :---- | :----------- |
| `coverage_score` | `float` | Fraction of competency questions answerable by the ontology |
| `completeness_score` | `float` | Average of class and property completeness scores |
| `gaps` | `List[str]` | Identified gaps in coverage |
| `suggestions` | `List[str]` | Improvement suggestions |
| `metrics` | `dict` | Detailed sub-metrics |
Every evaluator is a plain function `fn(actual, expected, config=None) -> EvalMetric`
registered under a stable name. `list_evaluators()` returns the current set:
- [Semantic Extract](semantic_extract) — Extraction module.
- [Knowledge Graph](kg) — Graph quality assessment.
- [Pipeline](pipeline) — Pipeline performance metrics.
- [Ontology Evaluator](ontology) — Available now for ontology quality metrics.
```python
>>> list_evaluators()
['decision_scores', 'exact_match', 'keyword_check', 'length_range',
'levenshtein', 'llm_as_judge', 'numeric_range', 'regex_match', 'rouge',
'temporal_range']
```
| Name | Passes when | Relevant `config` keys |
| :--- | :--- | :--- |
| `exact_match` | `actual == expected` | none |
| `regex_match` | `re.search(expected, actual)` matches | none |
| `keyword_check` | every required term appears in `actual` (word-boundary) | `required` (falls back to `expected`) |
| `numeric_range` | `min <= actual <= max` | `min`, `max` (both required) |
| `temporal_range` | ISO datetime `actual` falls in `[min, max]` | `min`, `max` as ISO strings (both required) |
| `length_range` | `min <= len(actual) <= max` | `min` (default 0), `max` (required) |
| `levenshtein` | normalized similarity `>= threshold` | `threshold` (default 0.8) |
| `rouge` | ROUGE-1 F1 `> 0` and `>= threshold` | `threshold` (default 0.0) |
| `llm_as_judge` | caller-supplied `judge_fn(actual, expected)` returns truthy | `judge_fn` (required callable) |
| `decision_scores` | all configured sub-checks on a `Decision` pass | see below |
An evaluator that cannot run (bad regex, unparseable datetime, no `judge_fn`) returns an
`EvalMetric` with an `"error"` key in `meta` rather than raising. Evaluators that
require numeric bounds (`numeric_range`, `length_range`) instead return a failing
metric with a `"reason"` key when the bound is missing — they do not raise and do
not set `"error"`.
### `decision_scores`
`decision_scores` accepts a `Decision` (from `semantica.context.decision_models`)
or its dict form and runs a set of field-level and governance checks. The score is
the fraction of checks that passed; `passed` is `True` only when all of them did.
| Sub-check | Controlled by |
| :--- | :--- |
| Outcome matches | `expected_outcome` in config, or the case's `expected`; **skipped** when neither is set |
| Confidence in range | `min_confidence` (default 0.0), `max_confidence` (default 1.0); always run |
| `decision_maker`, `reasoning`, `scenario` non-empty | always run |
| Provenance present in metadata | `provenance_key` (default `"provenance"`); always run |
| Policy compliance | `policy_engine` and `policy_id` both set; skipped otherwise |
Passing `causal_chain_exists` in config raises `NotImplementedError`. That key is a
reserved slot for a future release.
## Running an evaluation
`evaluate()` takes a list of cases and a list of evaluator names. A case is either
a `(expected, actual)` tuple or a dict:
```python
{
"id": "loan-001", # optional, generated if absent
"expected": ..., # optional; some evaluators read it, some don't
"actual": ..., # the value under test
"config": {...}, # optional, per-evaluator settings for this case
"target_fn": callable, # optional, called with the case to produce `actual`
}
```
If `actual` is missing, the runner calls the case's `target_fn` (or the
`target_fn` passed to `evaluate()`) to produce it. Per-case `config` is deep-merged
over the top-level `config`, so a case can override one evaluator's settings
without discarding the rest.
```python
from datetime import datetime
from semantica.context.decision_models import Decision
from semantica.evals import evaluate
decision = Decision(
decision_id="d-1",
category="loan",
scenario="loan-request",
reasoning="vetted against lending policy v3",
outcome="approve",
confidence=0.87,
timestamp=datetime.now(),
decision_maker="approver-a",
metadata={"provenance": "workflow:loan/v3"},
)
cases = [
{
"id": "loan-001",
"actual": decision,
"config": {
"decision_scores": {
"expected_outcome": "approve",
"min_confidence": 0.7,
}
},
},
]
summary = evaluate(cases, ["decision_scores"])
print(summary.pass_rate) # 1.0
```
Evaluators run independently per case. If one raises, that case's `status` becomes
`"error"` and the exception text is captured in the metric's `meta`; the rest of
the run continues.
## Objectives
By default each evaluator decides its own pass / fail. An **objective** overrides
that verdict at the run level, keyed by evaluator name under `config`:
```python
# Raise levenshtein's bar from its default 0.8 to 0.9
evaluate(
[("apple", "aple")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "maximize", "threshold": 0.9}}},
)
# Lower is better
evaluate(
[("night", "nacht")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "minimize", "threshold": 0.5}}},
)
# Expect the metric NOT to match
evaluate(
[("ok", "ok")],
evaluators=["exact_match"],
config={"exact_match": {"objective": {"expect": False}}},
)
```
Rules:
- `maximize` with `threshold`: pass iff `score >= threshold`. `maximize` with no
threshold is a no-op and the evaluator's own verdict stands.
- `minimize` with `threshold`: pass iff `score <= threshold`. `minimize`
**requires** a threshold; omitting it raises `ValueError`.
- `expect` (`True` / `False`): pass iff `bool(score)` equals it. Cannot be combined
with `direction` or `threshold`, and must be a real boolean.
- A metric that already carries an `"error"` in its `meta` is unaffected by any
objective.
- Invalid objective config is validated for every case before any evaluator runs,
so a bad objective fails the whole run up front rather than partway through.
## Reading the summary
```python
summary = evaluate(cases, ["decision_scores"])
summary.total, summary.passed, summary.failed, summary.errors
summary.pass_rate # passed / total, or 1.0 for an empty case list
for case in summary.cases:
print(case.case_id, case.status) # status: "pass" | "fail" | "error"
for name, metric in case.metrics.items():
print(name, metric.score, metric.passed)
print(metric.meta.get("reasons", {})) # per-sub-check failure reasons
```
`EvalMetric` is frozen (`score: float`, `passed: bool`, `meta: dict`). `CaseResult`
is a namedtuple, and `EvalSummary` is a plain dataclass, so all three are
straightforward to serialize for logging or regression tracking.
## Notes
- `llm_as_judge` needs `config["judge_fn"]`, a callable
`judge_fn(actual, expected) -> bool` you supply. No LLM backend is imported
unless you pass one in.
- `decision_scores` governance checks are opt-in: policy compliance is only
evaluated when both `policy_engine` and `policy_id` are present.
## See also
- [Decision Intelligence](/guides/decision-intelligence) — producing the `Decision` records this module scores
- [Reasoning](/reference/reasoning) — inference output that reasoning-text evaluators can measure
- [Policy Engine](/guides/policy-engine) — the `policy_engine` used by `decision_scores`
- [Ontology Evaluator](/reference/ontology) — separate tooling for ontology quality metrics
+1 -1
View File
@@ -403,7 +403,7 @@ Semantic neighborhood requires node embeddings stored in node properties (keys `
**Session state lost after restart**
Session state is in-memory only. Use `POST /api/export` to save a JSON snapshot before shutting down.
- [Context](context) — Build and save the ContextGraph that Explorer loads.
- [Context](/reference/context) — Build and save the ContextGraph that Explorer loads.
- [Ontology](ontology) — Programmatic ontology management and SHACL generation.
- [Visualization](visualization) — Programmatic graph rendering without the Explorer server.
- [Export](export) — Export to RDF, Parquet, and other formats without launching a server.
+1 -1
View File
@@ -394,7 +394,7 @@ The `export_csv` convenience function delegates to `CSVExporter.export()`. For p
**Match your export format to your consumer.** Neo4j → `cypher`; ArangoDB → `aql`; Gephi/yEd → `graphml` or `gexf`; semantic web tools → `turtle` or `json-ld`; analytics pipelines → `parquet`; zero-copy IPC → `arrow`.
</Tip>
- [Triplet Store](triplet_store) — Store RDF exports in a SPARQL-queryable backend.
- [Triplet Store](/reference/triplet_store) — Store RDF exports in a SPARQL-queryable backend.
- [Ontology](ontology) — Export OWL ontologies.
- [Provenance](provenance) — Include provenance metadata in RDF exports.
- [Pipeline](pipeline) — Add export as a final pipeline step.
+3 -3
View File
@@ -503,7 +503,7 @@ stats = store.get_stats()
</Tab>
</Tabs>
- [KG Module](kg) — Build the graph before persisting it.
- [Triplet Store](triplet_store) — RDF triple store for semantic web and SPARQL queries.
- [KG Module](/reference/kg) — Build the graph before persisting it.
- [Triplet Store](/reference/triplet_store) — RDF triple store for semantic web and SPARQL queries.
- [Visualization](visualization) — Visualize graphs stored in any backend.
- [Context](context) — AgentContext uses GraphStore for memory retrieval.
- [Context](/reference/context) — AgentContext uses GraphStore for memory retrieval.
+1 -1
View File
@@ -646,7 +646,7 @@ from semantica.ingest import ingest_file
result = ingest_file("source_path", method="my_format")
```
- [Parse](parse) — Parse raw sources into structured text and tables.
- [Parse](/reference/parse) — Parse raw sources into structured text and tables.
- [Pipeline](pipeline) — Orchestrate ingest as the first pipeline step.
- [Snowflake Integration](../integrations/snowflake) — Snowflake-specific setup and authentication guide.
- [Databricks Integration](../integrations/databricks) — Databricks Unity Catalog setup, authentication, and lineage guide.
+6 -6
View File
@@ -75,10 +75,10 @@ kg = builder.build({"entities": entities, "relationships": relationships})
## Temporal Knowledge Graphs (v0.4.0+)
<Info>
Full temporal reference including `BiTemporalFact`, `TemporalReasoningEngine`, Allen interval algebra, and `TemporalNormalizer` is covered in the dedicated [Temporal Intelligence](temporal) page. This section documents the KG-layer temporal API.
Full temporal reference including `BiTemporalFact`, `TemporalReasoningEngine`, Allen interval algebra, and `TemporalNormalizer` is covered in the dedicated [Temporal Intelligence](/reference/temporal) page. This section documents the KG-layer temporal API.
</Info>
The temporal stack — see the [Temporal Intelligence](temporal) page for the full reference.
The temporal stack — see the [Temporal Intelligence](/reference/temporal) page for the full reference.
### Building a Temporal Graph
@@ -264,7 +264,7 @@ versioner.verify_checksum(past_kg)
```
<Tip>
See the [Temporal Intelligence](temporal) reference for the full class API, domain examples (personnel changes, policy evolution, financial timelines), and configuration options.
See the [Temporal Intelligence](/reference/temporal) reference for the full class API, domain examples (personnel changes, policy evolution, financial timelines), and configuration options.
</Tip>
@@ -475,10 +475,10 @@ kg:
default_validity: infinite
```
- [Graph Store](graph_store) — Persist graphs in Neo4j, FalkorDB, or Apache AGE.
- [Semantic Extract](semantic_extract) — Source of entities and relationships fed to GraphBuilder.
- [Graph Store](/reference/graph_store) — Persist graphs in Neo4j, FalkorDB, or Apache AGE.
- [Semantic Extract](/reference/semantic_extract) — Source of entities and relationships fed to GraphBuilder.
- [Visualization](visualization) — Visualize knowledge graphs interactively.
- [Conflicts](conflicts) — Conflict detection and resolution.
- [Conflicts](/reference/conflicts) — Conflict detection and resolution.
### Cookbooks
+2 -2
View File
@@ -439,7 +439,7 @@ extractor = NERExtractor(
)
```
- [Semantic Extract](semantic_extract) — Use LLMs for NER and relation extraction.
- [Semantic Extract](/reference/semantic_extract) — Use LLMs for NER and relation extraction.
- [Agno Integration](../integrations/agno) — LLM providers in Agno multi-agent teams.
- [Reasoning](reasoning) — LLM-backed deductive and abductive reasoning.
- [Context](context) — GraphRAG uses LLMs for reasoning over knowledge graphs.
- [Context](/reference/context) — GraphRAG uses LLMs for reasoning over knowledge graphs.
+54 -6
View File
@@ -6,7 +6,7 @@ icon: "plug"
**`semantica.mcp_server`** exposes Semantica's knowledge graph, decision intelligence, semantic extraction, and reasoning capabilities as an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) **server over stdio**:
- 12 MCP tools exposed: extract entities, query graph, record decisions, run reasoning, export results
- 15 MCP tools exposed: extract entities, query graph, record decisions, run reasoning, export results
- No Python code required after launch: configure once, use from any MCP-aware client
- Compatible with Claude Desktop, Windsurf, Cline, Continue, VS Code, Roo Code, Cursor
@@ -40,12 +40,12 @@ python -m semantica.mcp_server
## What You Get
- **12 MCP Tools** — Extract entities, extract relations, record decisions, query decisions, find precedents, trace causal chains, add entities, add relationships, run analytics, summarise graph, run reasoning, export graph.
- **15 MCP Tools** — Extract entities, extract relations, record decisions, query decisions, find precedents, trace causal chains, add entities, add relationships, run analytics, summarise graph, run reasoning, export graph, query the live graph, update nodes, archive nodes.
- **3 Readable Resources** — Live graph JSON (`semantica://graph/summary`), decision list, and schema/version info: readable by any MCP client.
- **Zero Infrastructure** — Runs over stdio: no server, no port, no Docker required. One config block to activate in any MCP client.
- **Persistent Graphs** — Point `SEMANTICA_KG_PATH` at a saved graph file to reload it automatically on every server startup.
- **Decision Intelligence** — Record decisions, find precedents via hybrid similarity search, and trace causal chains across agent runs.
- **REST Alternative** — The [Explorer](explorer) module offers a full HTTP API and browser dashboard if you prefer programmatic access.
- **REST Alternative** — The [Explorer](/reference/explorer) module offers a full HTTP API and browser dashboard if you prefer programmatic access.
## Installation
@@ -159,7 +159,7 @@ The MCP server is included in the base install: no extras required.
## Tools
The MCP server exposes 12 tools that any connected AI assistant can call:
The MCP server exposes 15 tools that any connected AI assistant can call:
| Tool | Category | Description |
| :---- | :-------- | :----------- |
@@ -173,6 +173,9 @@ The MCP server exposes 12 tools that any connected AI assistant can call:
| `add_relationship` | Graph Operations | Add a directed edge between two nodes |
| `get_graph_summary` | Graph Operations | Node count, decision count, graph status |
| `get_graph_analytics` | Graph Operations | PageRank centrality and community detection |
| `query_graph` | Graph Operations | Fetch a node, traverse its neighbours, or keyword-search nodes |
| `update_node` | Graph Operations | Merge properties onto a node and persist to `SEMANTICA_KG_PATH` |
| `delete_node` | Graph Operations | Soft-delete (archive) a node and persist to `SEMANTICA_KG_PATH` |
| `run_reasoning` | Reasoning | Forward-chain IF/THEN rules over facts |
| `export_graph` | Reasoning & Export | Serialise the graph (`turtle`/`ttl`: RDF Turtle aliases, `nt`, `xml`, `json-ld`, `json`) |
@@ -386,6 +389,51 @@ Takes no input parameters.
</Accordion>
<Accordion title="query_graph" icon="magnifying-glass">
Read the live graph in one of three modes, set by `mode`:
- `node` — return a single node by `node_id`.
- `neighbors` (default) — traverse outward and inward from `node_id` up to `depth` hops (clamped to 1-5, default 1). Optional `relationship_types` filters edge types; optional `limit` caps results.
- `search` — keyword match `query` against each node's id and content. Optional `node_type` restricts the scan; `limit` defaults to 50.
**Input:**
```json
{ "mode": "neighbors", "node_id": "apple_inc", "depth": 2 }
```
</Accordion>
<Accordion title="update_node" icon="pen">
Merge a set of properties onto an existing node. The change is applied in memory and, when `SEMANTICA_KG_PATH` is set, written back to that file so it survives a restart. Returns `persisted: false` when no path is configured.
**Input:**
```json
{
"node_id": "task_42",
"properties": { "status": "done", "note": "shipped in v0.6.7" }
}
```
`node_id` and a non-empty `properties` object are required. Updating a missing node returns an error.
</Accordion>
<Accordion title="delete_node" icon="box-archive">
Soft-delete a node: it stays in the graph for history but is marked `status: "archived"`. Persists to `SEMANTICA_KG_PATH` when configured.
**Input:**
```json
{ "node_id": "task_42" }
```
</Accordion>
</AccordionGroup>
### Reasoning
@@ -445,7 +493,7 @@ The MCP server exposes three readable resources:
| `semantica://decisions/list` | All recorded decisions (up to 50) |
| `semantica://schema/info` | Server version and available tools |
- [Context](context) — The ContextGraph that the MCP server operates on.
- [Semantic Extract](semantic_extract) — NER and relation extraction powering the MCP tools.
- [Context](/reference/context) — The ContextGraph that the MCP server operates on.
- [Semantic Extract](/reference/semantic_extract) — NER and relation extraction powering the MCP tools.
- [Reasoning](reasoning) — Forward-chaining engine behind run_reasoning.
- [Agno Integration](../integrations/agno) — Use Semantica inside Agno multi-agent teams.
+2 -2
View File
@@ -584,7 +584,7 @@ normalized = normalize_text("Apple Inc.", method="expand_suffixes")
# → "Apple Incorporated"
```
- [Parse](parse) — Parse documents before normalization.
- [Split](split) — Chunk normalized text for embedding.
- [Parse](/reference/parse) — Parse documents before normalization.
- [Split](/reference/split) — Chunk normalized text for embedding.
- [Deduplication](deduplication) — Resolve duplicate entities after normalization.
- [Pipeline](pipeline) — Include normalization as a named pipeline step.
+2 -2
View File
@@ -287,6 +287,6 @@ ontology_data = ingest_ontology("schema.jsonld") # JSON-LD
</Note>
- [Reasoning](reasoning) — Apply inference rules over ontology axioms.
- [Knowledge Graph](kg) — The graph being modeled by the ontology.
- [Knowledge Graph](/reference/kg) — The graph being modeled by the ontology.
- [Export](export) — Export ontologies as RDF, OWL, or JSON-LD.
- [Conflicts](conflicts) — Detect ontology constraint violations.
- [Conflicts](/reference/conflicts) — Detect ontology constraint violations.
+2 -2
View File
@@ -298,6 +298,6 @@ for source in sources:
</Note>
- [Ingest](ingest) — Load files before parsing.
- [Split](split) — Chunk parsed text for embedding and extraction.
- [Split](/reference/split) — Chunk parsed text for embedding and extraction.
- [Docling Integration](../integrations/docling) — Full Docling integration setup guide.
- [Semantic Extract](semantic_extract) — Extract entities and relations from parsed text.
- [Semantic Extract](/reference/semantic_extract) — Extract entities and relations from parsed text.
+3 -3
View File
@@ -497,7 +497,7 @@ result = engine.execute_pipeline(
## SPARQL CONSTRUCT Template Steps
Use the `"construct_template"` step type to render and execute a [SPARQL CONSTRUCT template](triplet_store#sparql-construct-templates) as part of a pipeline. `store_backend` and `construct_template_registry` are execution-time resources, not step config — pass them to `execute_pipeline()`, the same way `delta_mode` steps receive `version_manager` and `triplet_store`:
Use the `"construct_template"` step type to render and execute a [SPARQL CONSTRUCT template](/reference/triplet_store#sparql-construct-templates) as part of a pipeline. `store_backend` and `construct_template_registry` are execution-time resources, not step config — pass them to `execute_pipeline()`, the same way `delta_mode` steps receive `version_manager` and `triplet_store`:
```python
from semantica.pipeline import PipelineBuilder, ExecutionEngine
@@ -589,6 +589,6 @@ StepStatus.SKIPPED # Skipped due to FailureHandler "skip" strategy
</AccordionGroup>
- [Ingest](ingest) — First step in most pipelines.
- [Semantic Extract](semantic_extract) — Core extraction step.
- [Knowledge Graph](kg) — Graph construction step.
- [Semantic Extract](/reference/semantic_extract) — Core extraction step.
- [Knowledge Graph](/reference/kg) — Graph construction step.
- [Export](export) — Final output step.
+2 -2
View File
@@ -522,7 +522,7 @@ Provenance tracking in Semantica produces the following audit artifacts:
`ProvenanceManager` does not include built-in Turtle or JSON-LD serialization. Use `entry.to_dict()` and `get_lineage()` to retrieve provenance data, then serialize with your preferred RDF library if W3C PROV-O RDF output is required.
</Note>
- [Change Management](change_management) — Version control and snapshot audit trails.
- [Change Management](/reference/change_management) — Version control and snapshot audit trails.
- [Ingest](ingest) — Provenance begins at the ingestion stage.
- [Export](export) — Include provenance metadata in RDF exports.
- [Context](context) — Decision provenance via AgentContext.
- [Context](/reference/context) — Decision provenance via AgentContext.
+3 -3
View File
@@ -482,7 +482,7 @@ step.confidence # float
`GraphReasoner` requires a configured LLM provider. If the provider fails to initialize, `reason()` returns an error string instead of raising. Check `reasoner.provider is not None` before calling if you need to surface failures explicitly.
</Warning>
- [Knowledge Graph](kg) — The knowledge graph being reasoned over.
- [Knowledge Graph](/reference/kg) — The knowledge graph being reasoned over.
- [Ontology](ontology) — Ontology axioms and SHACL constraints for logical reasoning.
- [Triplet Store](triplet_store) — RDF backend for SPARQL-based reasoning.
- [Context](context) — Reasoning integrated into agent decision intelligence.
- [Triplet Store](/reference/triplet_store) — RDF backend for SPARQL-based reasoning.
- [Context](/reference/context) — Reasoning integrated into agent decision intelligence.
+1 -1
View File
@@ -322,6 +322,6 @@ export SEMANTICA_SEED_MERGE_STRATEGY=seed_first
</Tip>
- [Ingest](ingest) — Load unstructured data alongside seed data.
- [Knowledge Graph](kg) — The target graph that seed data populates.
- [Knowledge Graph](/reference/kg) — The target graph that seed data populates.
- [Deduplication](deduplication) — Handle duplicates during seed-extracted merge.
- [Pipeline](pipeline) — Incorporate seed loading as a named pipeline step.
+3 -3
View File
@@ -410,7 +410,7 @@ triplets = trip.extract(text)
| `ml` | Fast | Free | High | Limited |
| `llm` | Medium | API cost | Highest | Yes (schema) |
- [LLM Providers](llms) — Configure which LLM is used for extraction.
- [Knowledge Graph](kg) — Build graphs from extracted entities and relationships.
- [Parse Module](parse) — Parse documents before extraction.
- [LLM Providers](/reference/llms) — Configure which LLM is used for extraction.
- [Knowledge Graph](/reference/kg) — Build graphs from extracted entities and relationships.
- [Parse Module](/reference/parse) — Parse documents before extraction.
- [Deduplication](deduplication) — Resolve duplicate entities after extraction.
+3 -3
View File
@@ -373,7 +373,7 @@ for chunk in chunks:
For the full pipeline orchestration API, see the [Pipeline reference](pipeline).
- [Parse](parse) — Parse documents before chunking: produces sections and metadata.
- [Embeddings](embeddings) — Embed chunks for vector search and semantic chunking.
- [Semantic Extract](semantic_extract) — Extract entities and relations from individual chunks.
- [Parse](/reference/parse) — Parse documents before chunking: produces sections and metadata.
- [Embeddings](/reference/embeddings) — Embed chunks for vector search and semantic chunking.
- [Semantic Extract](/reference/semantic_extract) — Extract entities and relations from individual chunks.
- [Pipeline](pipeline) — Integrate splitting as a named pipeline step.
+2 -2
View File
@@ -874,8 +874,8 @@ kg:
engine: allen # allen | point_in_time_only
```
- [Knowledge Graph Module](kg) — Core graph construction, `GraphBuilder`, analytics.
- [Context Module](context) — Decision temporal windows and `find_active_nodes()`.
- [Knowledge Graph Module](/reference/kg) — Core graph construction, `GraphBuilder`, analytics.
- [Context Module](/reference/context) — Decision temporal windows and `find_active_nodes()`.
- [Provenance](provenance) — W3C PROV-O lineage stamped alongside temporal metadata.
- [Export](export) — OWL, Turtle, JSON-LD, and Parquet export with temporal annotations.
+1 -1
View File
@@ -564,4 +564,4 @@ for row in result.bindings:
- [Export](export) — Export knowledge graphs to RDF formats.
- [Ontology](ontology) — Load OWL ontologies and store as RDF triples.
- [Reasoning](reasoning) — SPARQL-based property chain inference.
- [Graph Store](graph_store) — Property graph alternative for Cypher queries.
- [Graph Store](/reference/graph_store) — Property graph alternative for Cypher queries.
+1 -1
View File
@@ -222,5 +222,5 @@ from semantica.utils import read_json_file
config = read_json_file("config.json")
```
- [Core](core) — Framework orchestration that uses Utils internally.
- [Core](/reference/core) — Framework orchestration that uses Utils internally.
- [Pipeline](pipeline) — Uses ProgressTracker for per-step tracking.
+3 -3
View File
@@ -588,7 +588,7 @@ store.create_index(index_type="pq", metric="L2", m=8)
</Tab>
</Tabs>
- [Embeddings](embeddings) — Generate the vectors stored here.
- [Context](context) — AgentContext uses VectorStore for memory retrieval.
- [Split](split) — Chunk documents before embedding and storing.
- [Embeddings](/reference/embeddings) — Generate the vectors stored here.
- [Context](/reference/context) — AgentContext uses VectorStore for memory retrieval.
- [Split](/reference/split) — Chunk documents before embedding and storing.
- [Ingest](ingest) — Ingest documents before embedding and storing.
+4 -4
View File
@@ -288,9 +288,9 @@ For a full browser-based UI with search, path finding, and the Ontology Hub, lau
semantica-explorer --graph my_graph.json
```
See the [Explorer reference](explorer) for the full feature set and REST API.
See the [Explorer reference](/reference/explorer) for the full feature set and REST API.
- [Knowledge Graph](kg) — The graph being visualized.
- [Knowledge Graph](/reference/kg) — The graph being visualized.
- [Ontology](ontology) — Visualize ontology class structure.
- [Embeddings](embeddings) — Generate the embeddings visualized here.
- [Explorer](explorer) — Full interactive Knowledge Explorer UI.
- [Embeddings](/reference/embeddings) — Generate the embeddings visualized here.
- [Explorer](/reference/explorer) — Full interactive Knowledge Explorer UI.
@@ -0,0 +1,338 @@
# Objective Layer for semantica.evals Runner — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add per-metric objective support (direction + threshold, or Boolean expectation) to the `evaluate()` runner, overriding evaluator default pass verdicts, backward-compatible when no objective is configured.
**Architecture:** The runner already iterates evaluators and computes per-case status. Objectives are read from `config["<name>"]["objective"]`, validated up front, and applied to each returned metric's `passed` field (and `details`) before aggregation. Error metrics always win over objectives.
**Tech Stack:** Python 3.8+, stdlib only (typing, dataclasses). pytest for tests.
## Global Constraints
- Python >= 3.8: use `typing.Dict/List/Optional/Union`, never builtin generics or `|`.
- Zero new dependencies.
- Do not change the `EvalMetric` shape, the `evaluate()` signature, or the evaluator function signature.
- Existing behavior with no `objective` configured must be byte-for-byte unchanged (all 62 existing tests keep passing).
- Error metrics (`meta` contains `"error"`) always classify the case as `error`, regardless of objective.
- Config errors are programmer errors: raise `ValueError` from `evaluate()` before any evaluator runs (fail-fast).
- Tests go in `tests/evals/`, pytest class style, no new files outside the listed paths.
---
### Task 1: Objective parsing, validation, and re-decision in the runner
**Files:**
- Modify: `semantica/evals/runner.py`
- Test: `tests/evals/test_runner.py`
**Interfaces:**
- Consumes: `EvalMetric` from `.types` (fields: `score`, `passed`, `meta`); `evaluate(cases, evaluators, config=None, target_fn=None)` existing signature.
- Produces: private helpers `_parse_objective(name, eval_config) -> Optional[Dict]` (returns `None` when no objective configured, raises `ValueError` on invalid config) and `_apply_objective(metric, objective) -> bool` (returns the re-decided `passed`). Public `evaluate()` behavior extended as specified.
- [ ] **Step 1: Write the failing tests**
Append a new test class to `tests/evals/test_runner.py`:
```python
class TestObjective:
def test_maximize_with_threshold_pass(self):
# levenshtein similarity 1.0 for identical, objective demands >= 0.5
result = evaluate(
[("apple", "apple")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "maximize", "threshold": 0.5}}},
)
assert result.cases[0].status == "pass"
assert result.cases[0].metrics["levenshtein"].passed is True
def test_maximize_with_threshold_fail(self):
result = evaluate(
[("apple", "aple")], # similarity < 1.0
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "maximize", "threshold": 0.99}}},
)
assert result.cases[0].status == "fail"
assert result.cases[0].metrics["levenshtein"].passed is False
assert "levenshtein" in result.cases[0].details
def test_minimize_with_threshold_pass(self):
# edit distance normalized ~0.2; objective: distance <= 0.5
result = evaluate(
[("night", "nacht")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "minimize", "threshold": 0.5}}},
)
assert result.cases[0].status == "pass"
assert result.cases[0].metrics["levenshtein"].passed is True
def test_minimize_with_threshold_fail(self):
result = evaluate(
[("night", "nacht")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "minimize", "threshold": 0.1}}},
)
assert result.cases[0].status == "fail"
def test_expect_true_on_boolean_metric(self):
result = evaluate(
[("ok", "ok")],
evaluators=["exact_match"],
config={"exact_match": {"objective": {"expect": True}}},
)
assert result.cases[0].status == "pass"
def test_expect_false_overrides_passing_metric(self):
# exact_match passes (score 1.0) but expectation is false -> fail
result = evaluate(
[("ok", "ok")],
evaluators=["exact_match"],
config={"exact_match": {"objective": {"expect": False}}},
)
assert result.cases[0].status == "fail"
assert result.cases[0].metrics["exact_match"].passed is False
assert "exact_match" in result.cases[0].details
def test_maximize_without_threshold_is_noop(self):
# identical behavior to no objective: evaluator's own verdict stands
result = evaluate(
[("ok", "no")],
evaluators=["exact_match"],
config={"exact_match": {"objective": {"direction": "maximize"}}},
)
assert result.cases[0].status == "fail"
def test_minimize_without_threshold_raises(self):
with pytest.raises(ValueError):
evaluate(
[("a", "b")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "minimize"}}},
)
def test_bad_direction_raises(self):
with pytest.raises(ValueError):
evaluate(
[("a", "b")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "sideways", "threshold": 0.5}}},
)
def test_expect_with_direction_raises(self):
with pytest.raises(ValueError):
evaluate(
[("a", "b")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"expect": True, "direction": "maximize"}}},
)
def test_error_metric_wins_over_objective(self):
result = evaluate(
[("[invalid", "x")],
evaluators=["regex_match"],
config={"regex_match": {"objective": {"direction": "maximize", "threshold": 0.0}}},
)
assert result.cases[0].status == "error"
assert result.errors == 1
assert result.failed == 0
def test_no_objective_unchanged(self):
result = evaluate([("ok", "no")], evaluators=["exact_match"])
assert result.cases[0].status == "fail"
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `python3 -m pytest tests/evals/test_runner.py -q`
Expected: the new `TestObjective` tests fail (objective config ignored → `exact_match` passes under `expect:false` etc.); the pre-existing tests in the file still pass.
- [ ] **Step 3: Implement objective parsing, validation, and re-decision**
In `semantica/evals/runner.py`, add two helpers before `evaluate` and wire them into the evaluator loop.
```python
def _parse_objective(name, eval_config):
"""Return the validated objective dict, or None when not configured.
Raises ValueError for invalid configurations (programmer error).
"""
objective = (eval_config or {}).get("objective")
if objective is None:
return None
direction = objective.get("direction")
threshold = objective.get("threshold")
expect = objective.get("expect")
if expect is not None:
if direction is not None or threshold is not None:
raise ValueError(
f"objective for '{name}': 'expect' cannot be combined with "
"'direction' or 'threshold'"
)
return {"expect": bool(expect)}
if direction == "minimize":
if threshold is None:
raise ValueError(
f"objective for '{name}': 'minimize' requires a 'threshold'"
)
return {"direction": "minimize", "threshold": float(threshold)}
if direction == "maximize":
if threshold is None:
# no bar to re-decide against; treat as absent (evaluator default stands)
return None
return {"direction": "maximize", "threshold": float(threshold)}
raise ValueError(
f"objective for '{name}': 'direction' must be 'maximize' or 'minimize' "
f"(got {direction!r})"
)
def _apply_objective(metric, objective):
"""Return the objective-adjusted pass verdict for a non-error metric."""
if "expect" in objective:
return bool(metric.score) == objective["expect"]
if objective["direction"] == "minimize":
return metric.score <= objective["threshold"]
return metric.score >= objective["threshold"]
```
Then modify the evaluator loop in `evaluate()` so the parsed objective is computed once per case (outside the evaluator loop, since it only depends on merged config), and applied inside the loop:
```python
objective_by_name = {
name: _parse_objective(name, merged.get(name) or {})
for name in evaluators
}
metrics: Dict[str, EvalMetric] = {}
details: Dict[str, Any] = {}
failed, errored = False, False
for name in evaluators:
eval_config = merged.get(name) or {}
try:
metric = get_evaluator(name)(actual, expected, config=eval_config)
objective = objective_by_name.get(name)
if objective is not None and "error" not in metric.meta:
metric = EvalMetric(metric.score, _apply_objective(metric, objective), metric.meta)
metrics[name] = metric
if "error" in metric.meta:
errored = True
details[name] = metric.meta
elif not metric.passed:
failed = True
details[name] = metric.meta
except Exception as exc: # noqa: BLE001
errored = True
metrics[name] = EvalMetric(0.0, False, {"error": str(exc)})
details[name] = {"error": str(exc)}
```
Note: `objective_by_name` is computed once per case (it depends only on merged config), so invalid config raises `ValueError` at the first case — satisfying the fail-fast requirement. `EvalMetric` is a frozen dataclass, so the re-verdict constructs a new instance preserving score/meta.
- [ ] **Step 4: Run tests to verify they pass**
Run: `python3 -m pytest tests/evals/test_runner.py -q`
Expected: all `TestObjective` tests pass; pre-existing tests still pass.
- [ ] **Step 5: Run the full evals suite**
Run: `python3 -m pytest tests/evals -q`
Expected: 62 existing + new tests all pass (no regressions).
- [ ] **Step 6: Commit**
```bash
git add semantica/evals/runner.py tests/evals/test_runner.py
git commit -m "feat(evals): add per-metric objective support to runner"
```
---
### Task 2: Documentation — usage.md and CHANGELOG
**Files:**
- Modify: `semantica/evals/usage.md`
- Modify: `CHANGELOG.md`
**Interfaces:**
- Consumes: the objective config surface implemented in Task 1 (exact keys: `objective.direction`, `objective.threshold`, `objective.expect`; validation rules).
- Produces: docs only.
- [ ] **Step 1: Add objective section to usage.md**
Append a section after the existing "Run the runner over decision records" section:
```markdown
## Set per-evaluator objectives
By default each evaluator decides its own pass/fail. To override that
verdict at the run level, configure an **objective** per evaluator name:
```python
from semantica.evals import evaluate
# Require a minimum similarity (default direction is maximize):
evaluate(
[("apple", "aple")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "maximize", "threshold": 0.7}}},
)
# Lower is better — override the direction:
evaluate(
[("night", "nacht")],
evaluators=["levenshtein"],
config={"levenshtein": {"objective": {"direction": "minimize", "threshold": 0.5}}},
)
# Boolean expectation on a 0/1 metric:
evaluate(
[("ok", "ok")],
evaluators=["exact_match"],
config={"exact_match": {"objective": {"expect": False}}},
)
```
Rules:
- `maximize` + `threshold`: pass iff `score >= threshold`. `maximize` without
a threshold is a no-op (the evaluator's own verdict stands).
- `minimize` + `threshold`: pass iff `score <= threshold`. `minimize`
**requires** a threshold — omitting it raises `ValueError`.
- `expect` (`true`/`false`): pass iff `bool(score)` matches; cannot be
combined with `direction`/`threshold`.
- A metric whose `meta` contains `"error"` is always an error, never affected
by an objective.
- Invalid objective config raises `ValueError` before any evaluator runs.
```
- [ ] **Step 2: Add CHANGELOG entry**
Under `## [Unreleased]``### Added`, insert a new bullet at the top (before the `semantica.evals` module entry), following existing style:
```markdown
- **`semantica.evals` runner gains per-metric objectives** (#1091)
- `evaluate()` now accepts `config={"<evaluator>": {"objective": {"direction": "maximize"|"minimize", "threshold": X}}}` to override the evaluator's default pass verdict with a threshold; `{"objective": {"expect": bool}}` expresses a Boolean expectation
- `minimize` requires a `threshold`; `maximize` without one is a no-op; `expect` cannot be combined with `direction`/`threshold`; invalid config raises `ValueError` before any evaluator runs
- Error metrics are never affected by objectives (error wins over fail)
- Backward compatible: no `objective` key → existing behavior unchanged
- New tests in `tests/evals/test_runner.py::TestObjective`
```
- [ ] **Step 3: Verify docs examples run**
Run the three examples from Step 1 as a Python script (import `evaluate`, run each snippet) to confirm they don't raise unexpectedly. No test output assertion needed beyond "no exception" and sensible status values.
- [ ] **Step 4: Commit**
```bash
git add semantica/evals/usage.md CHANGELOG.md
git commit -m "docs(evals): document per-metric objectives"
```
---
## Self-Review Notes
- **Spec coverage:** §3.1 (config surface) → Task 1 helpers + Task 2 docs; §3.2 (semantics: maximize/minimize/expect) → Task 1 `_apply_objective`; §3.3 (error wins) → Task 1 error branch + `test_error_metric_wins_over_objective`; §3.4 rules 1-3 (validation) → Task 1 `_parse_objective` + 4 validation tests; §3.4 rule 4 → error branch; §3.5 (aggregation unchanged, details on final verdict) → Task 1 loop + `test_expect_false_overrides_passing_metric` asserts `details`; §4 (fail-fast ValueError) → `_parse_objective` at case top; §5 (tests) → Task 1 test class; §6 (compat) → `test_no_objective_unchanged` + full-suite green.
- **Type consistency:** `_parse_objective(name, eval_config) -> Optional[Dict]`, `_apply_objective(metric, objective) -> bool`; `EvalMetric(score, passed, meta)` positional construction preserved everywhere.
- **Backward compat:** objective parsed to `None` for absent config → loop behavior identical to before.
@@ -0,0 +1,115 @@
# Design: Objective layer for `semantica.evals` runner
**Date:** 2026-08-19
**Issue:** semantica-agi/semantica#1091 (assigned to pkupt)
**Base:** PR #1090 (`semantica.evals` module)
## 1. Problem
`semantica.evals` runs named evaluators and aggregates per-case pass/fail, but the pass judgement is hard-coded inside each evaluator — a higher score always means "better". There is no way to express an evaluation objective at the run level:
- apply a threshold the evaluator does not encode (e.g. "F1 must be ≥ 0.7");
- reverse the direction (e.g. "lower edit distance is better");
- express a Boolean expectation (e.g. "this metric should be `false`").
This blocks the domain-specific benchmark harnesses `docs/community-projects.md` says `semantica.evals` supports. Palantir AIP Evals models exactly this: each metric has an **objective** (Boolean expected value, or numeric `maximize`/`minimize` direction with an optional threshold), and a test case passes when **all** its metrics meet their objectives.
## 2. Scope
In scope:
- A per-metric objective configuration consumed by the `evaluate()` runner.
- Runner-level pass/fail re-decision for numeric scores and Boolean metrics.
- Backward-compatible behavior when no objective is configured.
- Tests and docs.
Out of scope:
- Changing the evaluator signature or the `EvalMetric` shape.
- Multi-iteration test cases (AIP Evals has them; Semantica's runner is single-iteration per case).
- Objective-aware aggregation beyond per-case `pass`/`fail` (existing `pass_rate` semantics are kept).
## 3. Design
### 3.1 Configuration surface
Objective is configured per evaluator inside the runner's `config`, under the evaluator name:
```python
config = {
"<evaluator_name>": {
"objective": {
"direction": "maximize" | "minimize",
"threshold": <float>, # optional
}
}
}
```
Boolean-form objective (shorthand): for metrics whose score is Boolean-like (0.0/1.0) or for semantic clarity, `{"objective": {"expect": true}}` / `{"objective": {"expect": false}}` is also supported.
### 3.2 Evaluation semantics
For each metric produced by an evaluator during a case run, if an objective exists for that evaluator name, the runner recomputes the metric's pass verdict:
- **maximize**: pass iff `score >= threshold`. If no `threshold` is given, the objective is treated as absent (evaluator's own verdict stands) — see 3.4 rule 2.
- **minimize**: pass iff `score <= threshold` (threshold required, see 3.4 rule 1).
- **expect**: pass iff `bool(score)` equals `expect` (for Boolean-style metrics).
When an objective is present, the runner **overrides** `metric.passed` with the objective verdict. When absent, `metric.passed` is used unchanged (existing behavior).
The `objective` key is a **reserved runner-level key**: it is consumed by the runner and is passed through to the evaluator function inside `eval_config` (evaluators already ignore unknown config keys via `cfg.get(...)`, so this is harmless); evaluators must not rely on it. The runner re-decision happens on the metric the evaluator returns, so no evaluator change is required.
### 3.3 Interaction with errors
An `EvalMetric` whose `meta` contains `"error"` remains classified as an error regardless of objective (error wins over fail, per the existing contract). Objectives only affect non-error metrics.
### 3.4 Ambiguity rules (explicit decisions)
1. **`minimize` without `threshold`** is rejected at config-validation time with a clear error (`ValueError`), because "lowest is best" has no absolute pass bar without a threshold. (AIP Evals allows direction-only; we require threshold to keep pass/fail well-defined.) — *Chosen for determinism; revisit if a use case demands direction-only minimize.*
2. **`maximize` without `threshold`** behaves like no objective (pass iff evaluator's own `passed`), because the evaluator's default is already "higher is better".
3. **`expect` with a numeric `direction`/`threshold`** is a config error (`ValueError`): pick one form.
4. **Objective on a metric that errors** → the error wins (3.3), objective ignored.
### 3.5 Aggregation
Unchanged:
- Case `status`: `"error"` if any metric errored, else `"fail"` if any failed, else `"pass"`.
- `pass_rate` = passed / total (1.0 on empty).
- `metrics` dict holds the (possibly re-verdict'd) `EvalMetric`; the re-verdict is observable via `metric.passed`.
- `details[name]` is populated when a metric ends up failed **after** objective re-decision (i.e. objective-failed metrics appear in `details`; metrics that pass under objective are not recorded there). This mirrors the existing "record failures in details" behavior applied to the final verdict.
### 3.6 Files
- `semantica/evals/runner.py` — add objective parsing/validation and re-decision inside the evaluator loop.
- `tests/evals/test_runner.py` — new test class(es) for objective semantics.
- `semantica/evals/usage.md` — document the objective config and examples.
- `CHANGELOG.md``[Unreleased]` entry.
No new dependencies; Python ≥ 3.8 (stdlib `typing`).
## 4. Error handling
- Invalid objective config (`direction` not in {maximize, minimize}, both `expect` and `direction`, `minimize` without threshold, non-numeric threshold) → `ValueError` raised at runner config parse, before any evaluator runs. Deterministic, fail-fast.
- These are programmer errors, not per-case data errors — no per-case `error` status involved.
## 5. Testing
New tests in `tests/evals/test_runner.py`:
1. maximize + threshold: score ≥ threshold → pass; below → fail.
2. minimize + threshold: score ≤ threshold → pass; above → fail (e.g. levenshtein on a close pair).
3. minimize without threshold → `ValueError`.
4. expect=true / expect=false on a Boolean metric (exact_match) — pass/fail per expectation.
5. no objective → existing behavior unchanged (evaluator's own verdict).
6. objective + error metric → error wins (status=error, not fail).
7. config error (bad direction) → `ValueError` raised by `evaluate()`.
8. objective turns a passing metric into failing → `details` records it; case status becomes fail.
9. backward-compat: all existing 62 tests keep passing.
## 6. Compatibility
- Public API (`evaluate`, `list_evaluators`, `get_evaluator`, types) unchanged in signature.
- `EvalMetric` shape unchanged (score, passed, meta) — only `passed` may be recomputed by the runner.
- Existing configs (no `objective` key) behave identically.
+33 -7
View File
@@ -8,8 +8,9 @@ Connects Claude Code, Cursor, Windsurf, Cline, Continue, VS Code (GitHub Copilot
## Quick start
```bash
# From the repo root
pip install -e ".[mcp]"
# From the repo root — no extra install flag needed; the root mcp/ package is
# part of the repository and does not require an external MCP SDK.
pip install -e .
# Test the server (type a JSON-RPC request, press Enter)
python -m mcp
@@ -89,7 +90,14 @@ python -m mcp [--debug]
## Per-tool configuration
### Claude Code (`~/.claude/settings.json`)
### Claude Code (`~/.claude.json` or `.mcp.json`)
Claude Code supports two MCP configuration scopes:
- **User scope**`~/.claude.json` applies across all projects for your user account.
- **Project scope**`.mcp.json` in your project root applies only to that project.
Both files use the same `mcpServers` structure:
```json
{
@@ -97,15 +105,33 @@ python -m mcp [--debug]
"semantica": {
"command": "python",
"args": ["-m", "mcp"],
"cwd": "/path/to/semantica"
"env": {
"PYTHONPATH": "/path/to/semantica"
}
}
}
}
```
Or use the plugin bundle:
> **Why `PYTHONPATH`?** The root `mcp/` package is intentionally not included in
> the installed wheel, so `python -m mcp` only works when the repository is on
> Python's import path. Setting `PYTHONPATH` here ensures this works regardless
> of the working directory Claude uses when it launches the server.
Or add it via the CLI (user scope):
```bash
claude mcp add semantica python -m mcp --cwd /path/to/semantica
claude mcp add --scope user semantica \
-e PYTHONPATH=/path/to/semantica \
-- python -m mcp
```
Or for project scope (omit `--scope user`):
```bash
claude mcp add semantica \
-e PYTHONPATH=/path/to/semantica \
-- python -m mcp
```
---
@@ -216,7 +242,7 @@ Add to your Q Developer MCP config:
| Variable | Default | Description |
|---|---|---|
| `SEMANTICA_KG_PATH` | *(in-memory)* | Path to persist/load the graph (JSON file) |
| `SEMANTICA_KG_PATH` | *(in-memory only)* | Path to a JSON file used to **load** the graph on startup and **persist** mutations (record decisions, add entities/relationships) back to disk after each change. When unset the graph lives in memory only and is lost when the server exits. |
---
+35 -7
View File
@@ -16,6 +16,13 @@ log = logging.getLogger("semantica.mcp.session")
_graph: Optional[Any] = None
# Tracks whether the last graph initialisation successfully loaded the
# configured SEMANTICA_KG_PATH file. When True (or no path was configured)
# mutation handlers are allowed to save. When False an existing file failed
# to load; saving would overwrite the original data with an empty graph, so
# persistence is blocked until the process is restarted with a readable file.
_load_ok: bool = True
def get_graph() -> Any:
"""
@@ -24,24 +31,45 @@ def get_graph() -> Any:
The graph is created with advanced_analytics=True so all centrality,
community-detection, and embedding features are available.
"""
global _graph
global _graph, _load_ok
if _graph is None:
from semantica.context import ContextGraph
_graph = ContextGraph(advanced_analytics=True)
_load_ok = True # default: safe to persist
kg_path = os.environ.get("SEMANTICA_KG_PATH", "").strip()
if kg_path and os.path.exists(kg_path):
try:
_graph.load(kg_path)
log.info("Graph loaded from %s", kg_path)
except Exception as exc:
log.warning("Could not load graph from %s: %s", kg_path, exc)
# Only attempt to load if the file has content. An empty file
# means the path was just created (e.g. a fresh tempfile) and
# should be treated as "start with empty graph" rather than a
# corrupt-file failure.
if os.path.getsize(kg_path) > 0:
try:
_graph.load_from_file(kg_path)
log.info("Graph loaded from %s", kg_path)
except Exception as exc:
log.warning(
"Could not load graph from %s: %s — persistence disabled "
"to protect existing data; restart the server to retry.",
kg_path, exc,
)
_load_ok = False # do not overwrite the original file
return _graph
def is_persistence_safe() -> bool:
"""Return True when it is safe to write mutations back to SEMANTICA_KG_PATH.
Returns False after a failed load so that mutation handlers do not
overwrite the original (possibly intact) file with a fresh empty graph.
"""
return _load_ok
def reset_graph() -> None:
"""Reset the singleton (mainly useful in tests)."""
global _graph
global _graph, _load_ok
_graph = None
_load_ok = True
+35 -1
View File
@@ -5,6 +5,7 @@ Decision intelligence tools — record, query, precedents, causal chain, impact.
from __future__ import annotations
import logging
import os
from mcp.schemas import (
ANALYZE_DECISION_IMPACT,
@@ -13,7 +14,7 @@ from mcp.schemas import (
QUERY_DECISIONS,
RECORD_DECISION,
)
from mcp.session import get_graph
from mcp.session import get_graph, is_persistence_safe
log = logging.getLogger("semantica.mcp.tools.decisions")
@@ -37,6 +38,39 @@ def handle_record_decision(args: dict) -> dict:
valid_from=args.get("valid_from"),
valid_until=args.get("valid_until"),
)
# Persist back to disk so the decision survives server restarts.
# Skip when the initial load failed to avoid overwriting original data.
kg_path = os.environ.get("SEMANTICA_KG_PATH", "").strip()
if kg_path:
if not is_persistence_safe():
# Roll back the in-memory mutation so the client-visible state
# matches the persisted state (neither is saved).
if hasattr(graph, "_decisions") and decision_id in graph._decisions:
del graph._decisions[decision_id]
if hasattr(graph, "_decision_index"):
cat = args.get("category", "")
if cat in graph._decision_index:
graph._decision_index[cat].discard(decision_id)
return {
"error": (
"Persistence blocked: the configured SEMANTICA_KG_PATH "
"could not be loaded at startup. Restart the server with "
"a readable graph file to re-enable persistence."
)
}
try:
graph.save_to_file(kg_path)
except Exception as save_exc:
# Atomic write failed. Roll back the in-memory mutation so the
# client-visible and persisted states remain consistent.
if hasattr(graph, "_decisions") and decision_id in graph._decisions:
del graph._decisions[decision_id]
if hasattr(graph, "_decision_index"):
cat = args.get("category", "")
if cat in graph._decision_index:
graph._decision_index[cat].discard(decision_id)
log.exception("save_to_file failed after record_decision; mutation rolled back")
return {"error": f"Mutation rolled back: could not persist graph: {save_exc}"}
return {
"decision_id": decision_id,
"status": "recorded",
+70 -1
View File
@@ -5,9 +5,10 @@ Graph tools — add entities/relationships, search, analytics, summary.
from __future__ import annotations
import logging
import os
from mcp.schemas import ADD_ENTITY, ADD_RELATIONSHIP, EMPTY, GET_ANALYTICS, SEARCH_GRAPH
from mcp.session import get_graph
from mcp.session import get_graph, is_persistence_safe
log = logging.getLogger("semantica.mcp.tools.graph")
@@ -25,6 +26,35 @@ def handle_add_entity(args: dict) -> dict:
node_type=args.get("type", "Entity"),
metadata=args.get("metadata", {}),
)
# Persist back to disk so the entity survives server restarts.
# Skip when the initial load failed to avoid overwriting original data.
kg_path = os.environ.get("SEMANTICA_KG_PATH", "").strip()
if kg_path:
if not is_persistence_safe():
# Roll back: remove the node we just added.
try:
with graph._lock:
graph._drop_node_from_indexes(node_id)
except Exception:
pass
return {
"error": (
"Persistence blocked: the configured SEMANTICA_KG_PATH "
"could not be loaded at startup. Restart the server with "
"a readable graph file to re-enable persistence."
)
}
try:
graph.save_to_file(kg_path)
except Exception as save_exc:
# Roll back: remove the node so in-memory and persisted state agree.
try:
with graph._lock:
graph._drop_node_from_indexes(node_id)
except Exception:
pass
log.exception("save_to_file failed after add_entity; mutation rolled back")
return {"error": f"Mutation rolled back: could not persist graph: {save_exc}"}
return {"status": "added", "id": node_id, "type": args.get("type", "Entity")}
except Exception as exc:
log.exception("add_entity failed")
@@ -46,6 +76,45 @@ def handle_add_relationship(args: dict) -> dict:
edge_type=rel_type,
metadata=args.get("metadata", {}),
)
# Persist back to disk so the relationship survives server restarts.
# Skip when the initial load failed to avoid overwriting original data.
kg_path = os.environ.get("SEMANTICA_KG_PATH", "").strip()
if kg_path:
if not is_persistence_safe():
# Roll back: remove the edge we just added (last matching edge).
try:
with graph._lock:
for edge in reversed(list(graph.edges)):
if (edge.source_id == source
and edge.target_id == target
and edge.edge_type == rel_type):
graph._drop_edge_from_indexes(edge)
break
except Exception:
pass
return {
"error": (
"Persistence blocked: the configured SEMANTICA_KG_PATH "
"could not be loaded at startup. Restart the server with "
"a readable graph file to re-enable persistence."
)
}
try:
graph.save_to_file(kg_path)
except Exception as save_exc:
# Roll back: remove the edge so in-memory and persisted state agree.
try:
with graph._lock:
for edge in reversed(list(graph.edges)):
if (edge.source_id == source
and edge.target_id == target
and edge.edge_type == rel_type):
graph._drop_edge_from_indexes(edge)
break
except Exception:
pass
log.exception("save_to_file failed after add_relationship; mutation rolled back")
return {"error": f"Mutation rolled back: could not persist graph: {save_exc}"}
return {"status": "added", "source": source, "target": target, "type": rel_type}
except Exception as exc:
log.exception("add_relationship failed")
+15 -12
View File
@@ -588,16 +588,15 @@ class AgentMemory:
return False
# Remove from vector store unless a caller is staging an atomic local update.
if not skip_vector:
if self.vector_store:
try:
vector_ids = list(self._vector_ids.get(memory_id, [])) or [
memory_id
]
self._delete_vector_ids(vector_ids)
except Exception as e:
self.logger.warning(f"Failed to delete from vector store: {e}")
self._vector_ids.pop(memory_id, None)
if not skip_vector and self.vector_store:
try:
vector_ids = list(self._vector_ids.get(memory_id, [])) or [memory_id]
self._delete_vector_ids(vector_ids)
except Exception as e:
self.logger.warning(f"Failed to delete from vector store: {e}")
# Bookkeeping runs unconditionally: a skip_vector delete still removes the
# item, so leaving its tracked ids behind would orphan them permanently.
self._vector_ids.pop(memory_id, None)
memory_item = self.memory_items[memory_id]
@@ -1588,12 +1587,16 @@ class AgentMemory:
memory_ids.append(memory_id)
return memory_ids
def batch_delete(self, memory_ids: List[str]) -> int:
def batch_delete(self, memory_ids: List[str], *, skip_vector: bool = False) -> int:
"""
Batch delete.
Args:
memory_ids: List of memory IDs to delete
skip_vector: If True, skip each item's own vector-store cascade
(see ``delete_memory``). A caller that is already erasing these
ids' vectors itself passes this to avoid a redundant,
best-effort delete against the vector store.
Returns:
Number of memories deleted
@@ -1603,7 +1606,7 @@ class AgentMemory:
"""
deleted = 0
for memory_id in memory_ids:
if self.delete_memory(memory_id):
if self.delete_memory(memory_id, skip_vector=skip_vector):
deleted += 1
return deleted
+24 -2
View File
@@ -1203,8 +1203,30 @@ class ContextGraph:
"links": links_data,
}
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# Write atomically: serialize to a sibling temp file then replace the
# destination in one OS-level rename. This guarantees the destination
# is either the old contents or the new contents — never a partial write
# — so a crash or disk-full error during json.dump cannot corrupt the
# sole persisted copy of the graph.
dest = Path(path)
dest.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(
dir=dest.parent, prefix=".kg_tmp_", suffix=".json"
)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, dest)
except Exception:
# Clean up the temp file on any failure so we don't litter the
# directory with partial writes.
try:
os.unlink(tmp_path)
except OSError:
pass
raise
self.logger.info(f"Saved context graph to {path}")
+62 -1
View File
@@ -34,6 +34,7 @@ Example:
'unsupported'
"""
import inspect
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
@@ -150,6 +151,24 @@ class ErasureCoordinator:
more than actually occurred. Erasing the graph last means a partial
failure leaves the node present and the receipt incomplete, which is
recoverable and honest.
Note:
An explicit ``vector_store=False`` also suppresses ``AgentMemory``'s
own internal vector cascade, not just the coordinator's leg (#1378).
``AgentMemory.delete_memory()`` deletes an item's vectors best-effort:
it catches a vector-store failure, logs it, and still returns ``True``,
so without this a caller who opted out of the vector leg could still
have ``memory.vector_store`` mutated underneath them while the receipt
read ``vectors: not_configured``. ``vector_store=False`` is taken to
mean "no vector activity at all", so the coordinator passes
``skip_vector=True`` through to ``memory.batch_delete()`` in that case,
and ``receipt.stores["vectors"]["status"]`` stays ``"not_configured"``
honestly -- the caller opted the vector store out entirely, rather than
the coordinator having erased it. This only applies when
``vector_store=False`` was passed explicitly; when no vector store
exists anywhere (no ``memory`` was supplied, or ``memory`` has no
``vector_store`` attribute), there is nothing to suppress and
``memory.batch_delete()`` is called as before.
"""
def __init__(
@@ -170,6 +189,12 @@ class ErasureCoordinator:
self.graph = graph
self.memory = memory
# Distinct from `self.vector_store is None`: that's also true when no
# vector store exists anywhere (no memory, or memory with no
# vector_store attribute), where there is nothing to suppress and
# forcing skip_vector onto a duck-typed memory would break callers
# whose batch_delete() doesn't accept that kwarg.
self._vector_leg_disabled = vector_store is False
if vector_store is False:
self.vector_store: Optional[Any] = None
elif vector_store is not None:
@@ -424,6 +449,20 @@ class ErasureCoordinator:
return {"status": STATUS_NOT_CONFIGURED}
deleted = 0
skip_vector = self._vector_leg_disabled and _accepts_skip_vector(
self.memory.batch_delete
)
if self._vector_leg_disabled and not skip_vector:
# The class docstring only requires find_by_entity/batch_delete; a
# duck-typed adapter is not required to support skip_vector. Falling
# back to the plain call keeps the memory leg working -- the
# adapter's own cascade (if it has one) just can't be suppressed.
self.logger.warning(
"Memory adapter %r has no skip_vector support; its own vector "
"cascade (if any) could not be suppressed for %r",
type(self.memory).__name__,
entity_id,
)
try:
# Sweep in pages until dry rather than passing one large limit:
# ``find_by_entity`` has historically defaulted to ``limit=10`` and
@@ -454,7 +493,10 @@ class ErasureCoordinator:
"detail": "memory items carry no 'memory_id'",
}
removed = self.memory.batch_delete(memory_ids)
if skip_vector:
removed = self.memory.batch_delete(memory_ids, skip_vector=True)
else:
removed = self.memory.batch_delete(memory_ids)
deleted += removed
if removed == 0:
# No progress: another page would return the same items.
@@ -564,6 +606,25 @@ def _memory_item_id(item: Any) -> Optional[str]:
return str(memory_id) if memory_id else None
def _accepts_skip_vector(batch_delete: Any) -> bool:
"""True when ``batch_delete`` takes a ``skip_vector`` keyword.
``skip_vector`` is an ``AgentMemory``-specific extension, not part of the
duck-typed contract the class docstring promises (``find_by_entity`` and
``batch_delete`` only). Passing it to an adapter that doesn't accept it
would raise ``TypeError`` and fail the whole memory leg, so this is
checked before ever passing the kwarg.
"""
try:
signature = inspect.signature(batch_delete)
except (TypeError, ValueError):
return False
for parameter in signature.parameters.values():
if parameter.name == "skip_vector" or parameter.kind == inspect.Parameter.VAR_KEYWORD:
return True
return False
#: Dict keys a backend uses to report whether a delete succeeded, and the
#: values that mean it did not. Qdrant returns ``{"status": <UpdateStatus>}``
#: and Pinecone ``{"deleted": True}``; neither is a bool, so a bare
+17 -6
View File
@@ -1,10 +1,21 @@
"""
Semantica Evals Module
"""Semantica Evals — evaluation layer for decision intelligence outputs.
Coming Soon
Provides a small library of deterministic and model-backed evaluators plus a
runner for measuring decision records, audit trails, and reasoning output.
"""
__version__ = "0.1.1"
__status__ = "coming_soon"
__all__ = []
from . import decision_evaluators # noqa: F401 (registers decision_scores)
from . import evaluators # noqa: F401 (registers the generic evaluators)
from .registry import get_evaluator, list_evaluators
from .runner import evaluate
from .types import CaseResult, EvalMetric, EvalSummary
__version__ = "0.1.0"
__all__ = [
"evaluate",
"get_evaluator",
"list_evaluators",
"CaseResult",
"EvalMetric",
"EvalSummary",
]

Some files were not shown because too many files have changed in this diff Show More