mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-02 04:00:40 +00:00
Compare commits
502
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac5015bc3f | ||
|
|
29c72f59b3 | ||
|
|
21c2f190f8 | ||
|
|
8ef67b8bda | ||
|
|
0439cf884d | ||
|
|
141bf80394 | ||
|
|
0c4e18e256 | ||
|
|
39045d783b | ||
|
|
afa54e1bbf | ||
|
|
a01b3c36fc | ||
|
|
0dda380580 | ||
|
|
e7c9f6e7f3 | ||
|
|
b828ebfef0 | ||
|
|
6330627ddb | ||
|
|
24e327d161 | ||
|
|
b9f3c59443 | ||
|
|
34955ba246 | ||
|
|
95b46f12b6 | ||
|
|
245ff76b99 | ||
|
|
a2b6a481dc | ||
|
|
c7edba88ea | ||
|
|
eb8598e0cf | ||
|
|
64d4157644 | ||
|
|
e1b1e63541 | ||
|
|
6b0a8e60ce | ||
|
|
8b22a58b8f | ||
|
|
ba286a55ea | ||
|
|
f610eff0ed | ||
|
|
e9e7278720 | ||
|
|
6b2cafd02e | ||
|
|
90d857a98f | ||
|
|
1861ca578c | ||
|
|
63acc7a66e | ||
|
|
00ceb09960 | ||
|
|
2909e0fa14 | ||
|
|
d2574990fa | ||
|
|
53d1da87c0 | ||
|
|
9e916a82b5 | ||
|
|
e8bf0e50d3 | ||
|
|
269fdaa9fb | ||
|
|
2d9bbf08b1 | ||
|
|
fbbe36983b | ||
|
|
877903358a | ||
|
|
070b36902b | ||
|
|
3b9efb7856 | ||
|
|
2a031f0225 | ||
|
|
d04f2b3643 | ||
|
|
2811469071 | ||
|
|
a0b4793590 | ||
|
|
cdeb04b816 | ||
|
|
b7e31d82b0 | ||
|
|
2eba54caac | ||
|
|
9fd1df9c51 | ||
|
|
4517089a7f | ||
|
|
9553e1a176 | ||
|
|
04fcfb61a7 | ||
|
|
5dc6966706 | ||
|
|
bb956b2735 | ||
|
|
e3a3f6010b | ||
|
|
b6373204e2 | ||
|
|
e385c78977 | ||
|
|
7b581d5960 | ||
|
|
07ca93fe5c | ||
|
|
41e430b928 | ||
|
|
438d8bc7af | ||
|
|
82f1f6bd10 | ||
|
|
c86570b996 | ||
|
|
93dda5e435 | ||
|
|
6ffed78fd9 | ||
|
|
f06de0dab2 | ||
|
|
dd016744ce | ||
|
|
379994867d | ||
|
|
7884d71e23 | ||
|
|
ca5f081793 | ||
|
|
6ad1502224 | ||
|
|
b010ba68fa | ||
|
|
7c8dfbd3c0 | ||
|
|
45400c88d3 | ||
|
|
26f6cdf9e5 | ||
|
|
96f88594c2 | ||
|
|
c0d08c46f7 | ||
|
|
5a388d0bcc | ||
|
|
f516aef8fd | ||
|
|
c9a382e676 | ||
|
|
897d950bdc | ||
|
|
dd8fa17db8 | ||
|
|
92801c220e | ||
|
|
738480606c | ||
|
|
f9e0bcf210 | ||
|
|
bb0e9f49e3 | ||
|
|
f95c1612d5 | ||
|
|
8c202a691e | ||
|
|
304b82fbd6 | ||
|
|
8d2dfaa53c | ||
|
|
39aaae778f | ||
|
|
16d628997a | ||
|
|
5e6ad6e87e | ||
|
|
f6198039fa | ||
|
|
983f5301e8 | ||
|
|
5a852169be | ||
|
|
fe6ca7fccb | ||
|
|
66c8431eee | ||
|
|
3e2a0a3f3b | ||
|
|
d22a54353a | ||
|
|
f165679c11 | ||
|
|
17460edca9 | ||
|
|
7e815920ac | ||
|
|
7f93eb7104 | ||
|
|
eec3e8804a | ||
|
|
9cb6073568 | ||
|
|
073c48882c | ||
|
|
be86d1b5db | ||
|
|
6f93f429c4 | ||
|
|
bc683e7a34 | ||
|
|
cda5310949 | ||
|
|
17f88ca600 | ||
|
|
658de23357 | ||
|
|
66e8964d22 | ||
|
|
892ff4b4a7 | ||
|
|
a88300d74f | ||
|
|
390152c78c | ||
|
|
17602812f9 | ||
|
|
523b02083f | ||
|
|
952a4530f5 | ||
|
|
2ce5067aa3 | ||
|
|
d056e47ab7 | ||
|
|
8eafd2d024 | ||
|
|
7ba93f6772 | ||
|
|
d466203761 | ||
|
|
730dea7911 | ||
|
|
47764c3033 | ||
|
|
055d2fd98d | ||
|
|
ce66681715 | ||
|
|
655b553262 | ||
|
|
60bf8ec75e | ||
|
|
a1478af9c4 | ||
|
|
ee4d6a9188 | ||
|
|
2d00257ae5 | ||
|
|
e78ad7f819 | ||
|
|
fdb347fe8a | ||
|
|
1cc2f6b93a | ||
|
|
9e26d96b3c | ||
|
|
7267425eb5 | ||
|
|
d9cf7b0088 | ||
|
|
09666806da | ||
|
|
9daddd8186 | ||
|
|
dc8d7ddb03 | ||
|
|
b34634c8b5 | ||
|
|
ee93c4bbe1 | ||
|
|
e4425818e4 | ||
|
|
7b31304e1e | ||
|
|
ab93ec3e8f | ||
|
|
f2eb3e1608 | ||
|
|
898a660ca7 | ||
|
|
fb1e6a6d6e | ||
|
|
8d0dce13c5 | ||
|
|
61c8435bd7 | ||
|
|
0d7b9ca1df | ||
|
|
aca15d3694 | ||
|
|
670027fd22 | ||
|
|
3ea1283626 | ||
|
|
b313604bde | ||
|
|
5e6df93f64 | ||
|
|
920c0e55d5 | ||
|
|
7de2a2eb5e | ||
|
|
ce60acb294 | ||
|
|
4acdefd4b8 | ||
|
|
a16cb9c468 | ||
|
|
1bdaad9c59 | ||
|
|
db00a3d1ad | ||
|
|
d8b8ae634b | ||
|
|
cdb26aab3b | ||
|
|
f4db4469ba | ||
|
|
98453cab5d | ||
|
|
1fde71768d | ||
|
|
e00bdfe8d4 | ||
|
|
9e31e8d746 | ||
|
|
9d680d4369 | ||
|
|
9d0744e20e | ||
|
|
0b52b715dc | ||
|
|
745927d674 | ||
|
|
af401c8566 | ||
|
|
2e2dae558f | ||
|
|
3a1a798107 | ||
|
|
a4b17dd72b | ||
|
|
9366f07239 | ||
|
|
61676fb321 | ||
|
|
1ea5e5c012 | ||
|
|
490d9c814b | ||
|
|
d2c20d410c | ||
|
|
67a8ab1a8e | ||
|
|
dfd7785cc1 | ||
|
|
ac4a200f26 | ||
|
|
c1f0cf6f34 | ||
|
|
665f9c080e | ||
|
|
6e6b190da1 | ||
|
|
e03ba5685c | ||
|
|
8d32932322 | ||
|
|
7a5e8fd981 | ||
|
|
082ab14d2e | ||
|
|
14d350378f | ||
|
|
241a24d75d | ||
|
|
b2eb5db87f | ||
|
|
74d5980215 | ||
|
|
1af17f3398 | ||
|
|
c964e11d38 | ||
|
|
8829aa5ce2 | ||
|
|
102274c668 | ||
|
|
3b400eb88b | ||
|
|
678d891b42 | ||
|
|
e60cef9eb7 | ||
|
|
79a980d956 | ||
|
|
47828cff0d | ||
|
|
17289121cb | ||
|
|
b670bc32a4 | ||
|
|
5af6e383ad | ||
|
|
cd70481034 | ||
|
|
1cf13d0188 | ||
|
|
069af2a038 | ||
|
|
35ebccbdd7 | ||
|
|
de432d5eb4 | ||
|
|
1d04005edf | ||
|
|
282418c953 | ||
|
|
c77690129d | ||
|
|
655a24b77e | ||
|
|
38b766298e | ||
|
|
ba050acc7d | ||
|
|
c0f106dc1e | ||
|
|
98a2cf9490 | ||
|
|
8faa87dace | ||
|
|
4747d403bc | ||
|
|
8348df63be | ||
|
|
68b8b370d6 | ||
|
|
f8ec5ac010 | ||
|
|
40fe1d587a | ||
|
|
29a608f60e | ||
|
|
907f0e8f45 | ||
|
|
0c213f1483 | ||
|
|
a51542ce40 | ||
|
|
08150fb2f7 | ||
|
|
ce01067009 | ||
|
|
a896c36389 | ||
|
|
5f55e9b363 | ||
|
|
25999076df | ||
|
|
2dbc50a2fe | ||
|
|
790ff71c0a | ||
|
|
af57e5269d | ||
|
|
9203c2d684 | ||
|
|
88309da972 | ||
|
|
719063e781 | ||
|
|
b0947df934 | ||
|
|
a036c4405b | ||
|
|
0365712a8b | ||
|
|
f7170cd6df | ||
|
|
60c00fb5c2 | ||
|
|
1b277dcdd7 | ||
|
|
3065f3c00e | ||
|
|
0f1d262327 | ||
|
|
6390138edc | ||
|
|
8b47c148c5 | ||
|
|
8eae75c03a | ||
|
|
9eb7ea97d0 | ||
|
|
cd07e02db6 | ||
|
|
dfb51f8b54 | ||
|
|
1d88c06cbb | ||
|
|
033eaab108 | ||
|
|
9a81034336 | ||
|
|
664e343914 | ||
|
|
f677b638e2 | ||
|
|
2537976e8f | ||
|
|
5cf49bf799 | ||
|
|
c4d72ee3fc | ||
|
|
7a879a3508 | ||
|
|
ebd2be3d9d | ||
|
|
bc33bf9340 | ||
|
|
77e50127c8 | ||
|
|
73af7d5bfc | ||
|
|
e30ef6cb76 | ||
|
|
cf7a78fa10 | ||
|
|
4f0cf282a1 | ||
|
|
ebfce8c5ab | ||
|
|
34bc7a45b9 | ||
|
|
71d037f581 | ||
|
|
6651386074 | ||
|
|
a219c2f44e | ||
|
|
dae368532c | ||
|
|
b282487b17 | ||
|
|
129edaf05b | ||
|
|
21c7933ec1 | ||
|
|
c46e531dcd | ||
|
|
0f0800f109 | ||
|
|
9e68266563 | ||
|
|
c66f8160dd | ||
|
|
b1e1c9f0d9 | ||
|
|
cef2b4314a | ||
|
|
b9af181625 | ||
|
|
05fa3247b7 | ||
|
|
42899c1416 | ||
|
|
19665b9db2 | ||
|
|
07dc579faf | ||
|
|
96e438c81c | ||
|
|
c483352d7a | ||
|
|
01ba7d113b | ||
|
|
cdd45331fa | ||
|
|
a3a0848577 | ||
|
|
eeda5f5b80 | ||
|
|
62b03d4fa5 | ||
|
|
8a2b07b864 | ||
|
|
0773e24075 | ||
|
|
8de7cc1b6d | ||
|
|
c6dc9d87aa | ||
|
|
781b103436 | ||
|
|
e4f0c8993c | ||
|
|
b2b823a5c3 | ||
|
|
3c863e860e | ||
|
|
4c74da7682 | ||
|
|
de84ab6d06 | ||
|
|
8be16f782e | ||
|
|
9faa5661f8 | ||
|
|
3e0fbf8e95 | ||
|
|
29f5c72533 | ||
|
|
e13ea740cd | ||
|
|
e2b79ada9a | ||
|
|
c6f8f0dd04 | ||
|
|
4147f0ca3b | ||
|
|
ad84cb5897 | ||
|
|
adac6f7a7b | ||
|
|
90e6baa0ec | ||
|
|
ca8a916373 | ||
|
|
0dd5c4b7f1 | ||
|
|
13eed9cf6d | ||
|
|
1064b0bdbe | ||
|
|
ac047f917a | ||
|
|
7b6e74d042 | ||
|
|
ba491d8cba | ||
|
|
54868fea80 | ||
|
|
a0e7e9a1c9 | ||
|
|
e8d71b49fa | ||
|
|
8916200d31 | ||
|
|
290916a6ff | ||
|
|
88bd7d6b05 | ||
|
|
44f817ee71 | ||
|
|
5d6051bee1 | ||
|
|
0b922e77c5 | ||
|
|
e3a0c84b90 | ||
|
|
6bbb8f929f | ||
|
|
d8bbd8877f | ||
|
|
b1e5c9e3c9 | ||
|
|
fe64b8ad8a | ||
|
|
65a00de408 | ||
|
|
e9a2f87325 | ||
|
|
e7a13f7de6 | ||
|
|
fedbd8de8e | ||
|
|
ed27b98c53 | ||
|
|
353a6c605d | ||
|
|
0659509c14 | ||
|
|
b2a2d24b14 | ||
|
|
e315ad849d | ||
|
|
62c7970b32 | ||
|
|
4235840a9e | ||
|
|
e3c33cf23b | ||
|
|
868109fa34 | ||
|
|
2e5ad9d28b | ||
|
|
6c61e34ad4 | ||
|
|
9a6c07417e | ||
|
|
89fe0df40b | ||
|
|
500d0239e0 | ||
|
|
e18e6d1a00 | ||
|
|
753bf18ce7 | ||
|
|
1aee4dfd29 | ||
|
|
572d2da64a | ||
|
|
3f55a34eff | ||
|
|
2dbc502720 | ||
|
|
c5fb2d24fd | ||
|
|
9c09851658 | ||
|
|
2cea2708a6 | ||
|
|
b80c91ccb9 | ||
|
|
ad9ea48d26 | ||
|
|
62f2c0af92 | ||
|
|
24166bbfa9 | ||
|
|
665771f230 | ||
|
|
9d4d682883 | ||
|
|
399416f6ea | ||
|
|
32d0fe105c | ||
|
|
ee7c00f655 | ||
|
|
dcf26dfa99 | ||
|
|
9bb71a45f2 | ||
|
|
4a3b1676d6 | ||
|
|
a6654ba570 | ||
|
|
060ff47826 | ||
|
|
4146fbf277 | ||
|
|
1d1ae398c4 | ||
|
|
99a4db3ece | ||
|
|
de03d05600 | ||
|
|
c077944457 | ||
|
|
b309451398 | ||
|
|
0c1bdc0cee | ||
|
|
c2a6e944fe | ||
|
|
0dc5eb6075 | ||
|
|
5a316a4641 | ||
|
|
39c9fc97b4 | ||
|
|
f043367a73 | ||
|
|
38ec333626 | ||
|
|
2d90bdaad5 | ||
|
|
43a8f823c8 | ||
|
|
7a7e3f9e6b | ||
|
|
c59e33c9d3 | ||
|
|
867ecfda1b | ||
|
|
4103f747c5 | ||
|
|
ad8f24fc6b | ||
|
|
7535e39c56 | ||
|
|
420ccfe45a | ||
|
|
ad72ab9d19 | ||
|
|
a06e029264 | ||
|
|
a4caafbb6d | ||
|
|
9a2b2b9cd1 | ||
|
|
c842af65d0 | ||
|
|
e8d0d7a2cf | ||
|
|
8ffaf6001b | ||
|
|
476267f764 | ||
|
|
7ebdbcc62b | ||
|
|
e8e838829d | ||
|
|
a7e43304fc | ||
|
|
b36f6cd9eb | ||
|
|
af93dd29a8 | ||
|
|
dc8c29a87f | ||
|
|
78c52eb099 | ||
|
|
6b847716b1 | ||
|
|
bcdf3c357a | ||
|
|
2be45a01f1 | ||
|
|
26b3b9bb1e | ||
|
|
9c99832486 | ||
|
|
0dd74f7666 | ||
|
|
94d9f70f41 | ||
|
|
d932cb1e5b | ||
|
|
fdea0762d6 | ||
|
|
5319e504e0 | ||
|
|
1d96b6f80e | ||
|
|
467955e98b | ||
|
|
ed6ff634b3 | ||
|
|
eacc00a544 | ||
|
|
5555c2afa5 | ||
|
|
246bcc96cd | ||
|
|
eb21b851df | ||
|
|
34df1964b9 | ||
|
|
8c4e5e5968 | ||
|
|
501142e8de | ||
|
|
4b1c78372c | ||
|
|
e0a7ab75af | ||
|
|
0dbdad35b9 | ||
|
|
8efc61e401 | ||
|
|
194a72d0f9 | ||
|
|
95c5690964 | ||
|
|
1405f85d62 | ||
|
|
bafc826e26 | ||
|
|
e3c17487e3 | ||
|
|
41b3a46de3 | ||
|
|
436bcc5352 | ||
|
|
49582ad89a | ||
|
|
f7f75e3132 | ||
|
|
0b54cce829 | ||
|
|
76b7e0a15b | ||
|
|
586964ce0e | ||
|
|
7b75cf6b6d | ||
|
|
64d806a271 | ||
|
|
176622441a | ||
|
|
fcaebe9bd4 | ||
|
|
095ba13b3b | ||
|
|
f16ccb3d1d | ||
|
|
a1b85e0ff8 | ||
|
|
e150f43ee4 | ||
|
|
59ff25fc06 | ||
|
|
dd08a8e633 | ||
|
|
1176183090 | ||
|
|
91b03874fc | ||
|
|
fd010f399d | ||
|
|
e4fb2ed47f | ||
|
|
bf32c016f2 | ||
|
|
22bb8569a7 | ||
|
|
93881daaae | ||
|
|
a735cc0538 | ||
|
|
930be04fed | ||
|
|
7ee19655d0 | ||
|
|
d180576285 | ||
|
|
7cf8676a83 | ||
|
|
fbe3b27342 | ||
|
|
96cb80245f | ||
|
|
223406d5b4 | ||
|
|
bd2cada0fb | ||
|
|
cc2e18d7ff | ||
|
|
bb1ac5eb99 | ||
|
|
91ba5219d0 | ||
|
|
14b3b6b19b | ||
|
|
343168df7a | ||
|
|
c196cb16d7 | ||
|
|
297f5b9473 | ||
|
|
1d3ecdc459 | ||
|
|
7caace7c5d | ||
|
|
2af0fe3214 | ||
|
|
e1c8bfacec | ||
|
|
60389a0e57 | ||
|
|
f5896574c6 |
@@ -0,0 +1 @@
|
||||
# Initialization
|
||||
@@ -0,0 +1 @@
|
||||
# Intialization
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: semantica
|
||||
description: Semantica full-stack knowledge graph skill for context graphs, decision intelligence, explainability, extraction, reasoning, visualization, ontology, provenance, policy, and export workflows.
|
||||
---
|
||||
|
||||
# Semantica
|
||||
|
||||
This Skill helps Claude apply Semantica knowledge graph capabilities to context graph analysis, decision intelligence, explainability, semantic extraction, graph analytics, reasoning, provenance, ontology, policy, ingestion, deduplication, and export.
|
||||
|
||||
## When to use this Skill
|
||||
|
||||
- The user asks about knowledge graphs, entities, relations, triplets, or semantic extraction.
|
||||
- A task requires context graph analysis, graph topology, centrality, communities, paths, or embeddings.
|
||||
- The request involves decision intelligence, causal influence, decision graphs, or outcome analysis.
|
||||
- The user asks for explainability, decision rationale, or transparency for graph results.
|
||||
- The request involves reasoning: deductive, abductive, SPARQL, Datalog, or Rete rules.
|
||||
- The user needs provenance, audit history, lineage tracking, or change tracing.
|
||||
- The request is about ontology modeling, schema validation, or policy enforcement.
|
||||
- Data must be ingested from files, databases, APIs, repositories, or MCP servers.
|
||||
- There is a need to deduplicate entities, normalize graph data, or merge duplicate graph objects.
|
||||
- The user wants to export graphs to JSON, RDF, Parquet, CSV, GraphML, or similar.
|
||||
|
||||
## What this Skill contains
|
||||
|
||||
- Semantic extraction guidance for NER, relation extraction, event detection, coreference resolution, and triplet generation.
|
||||
- Context graph and graph analytics workflows for topology, centrality, community detection, path finding, embeddings, and decision insights.
|
||||
- Decision intelligence support for causal reasoning, decision impact, decision graphs, and outcome analysis.
|
||||
- Explainability guidance for decision rationale, graph reasoning, rule traces, and result transparency.
|
||||
- Reasoning support for logic, hypotheses, SPARQL, Datalog, and rule-based inference.
|
||||
- Provenance and audit guidance for tracing sources, recording changes, and verifying graph lineage.
|
||||
- Ontology guidance for defining concepts, validating schemas, and modeling relationships.
|
||||
- Policy checks for compliance evaluation and graph governance.
|
||||
- Temporal analysis guidance for event timelines and graph evolution.
|
||||
- Deduplication support for duplicate detection, fuzzy matching, and graph cleanup.
|
||||
- Export workflows for sharing results in multiple structured formats.
|
||||
|
||||
## Best prompt patterns
|
||||
|
||||
Use clear task descriptions, and mention the desired output format when possible.
|
||||
|
||||
- "Extract entities, relations, and events from this text and summarize the resulting graph."
|
||||
- "Analyze this context graph and show the top 5 most influential nodes."
|
||||
- "Generate a decision intelligence report with causal impact and explainability."
|
||||
- "Run a provenance trace for node X and describe its history."
|
||||
- "Validate the ontology for this graph and report any schema problems."
|
||||
- "Ingest the data from this MCP server and merge it into the current graph."
|
||||
- "Export the graph to JSON and GraphML with node and edge metadata."
|
||||
|
||||
## How Claude should use this Skill
|
||||
|
||||
1. Read the YAML metadata and identify whether the request matches Semantica graph, context graph, decision intelligence, or extraction tasks.
|
||||
2. Load this Skill when the request mentions Semantica, knowledge graphs, context graphs, decision intelligence, explainability, reasoning, or provenance.
|
||||
3. Use the instructions here to choose the right workflow and then read additional files or scripts only if needed.
|
||||
|
||||
## Authoring note
|
||||
|
||||
This Skill is purposely concise and focused on task selection. It is not intended to include every detail; Claude should use the filesystem-based model to load any extra reference files only when asked.
|
||||
@@ -0,0 +1 @@
|
||||
# Initialization
|
||||
+1
-1
@@ -7,7 +7,7 @@ Check the [docs folder](https://github.com/Hawksight-AI/semantica/tree/main/docs
|
||||
|
||||
### 💬 Community Support
|
||||
- **GitHub Discussions**: [Ask questions](https://github.com/Hawksight-AI/semantica/discussions)
|
||||
- **Discord**: Join our [Discord server](https://discord.gg/ggb7vWeP) for real-time chat
|
||||
- **Discord**: Join our [Discord server](https://discord.gg/sV34vps5hH) for real-time chat
|
||||
|
||||
### 💭 Discussions
|
||||
Join the conversation on [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions):
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
name: "Semantica CodeQL Config"
|
||||
|
||||
# Exclude auto-generated notebook exports and bundled third-party JS.
|
||||
# Files in cookbook/**/*.html are self-contained Plotly/MapLibre bundles
|
||||
# produced by Jupyter nbconvert — they embed minified third-party libraries
|
||||
# (Plotly, MapLibre GL JS) whose internal patterns trigger false-positive JS
|
||||
# alerts (js/incomplete-url-substring-sanitization, js/insecure-randomness,
|
||||
# js/prototype-pollution-utility). These are not application code.
|
||||
paths-ignore:
|
||||
- "cookbook/**/*.html"
|
||||
- "cookbook/**/*.js"
|
||||
@@ -2,9 +2,16 @@ name: Semantica Performance Suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
performance-test:
|
||||
@@ -43,7 +50,7 @@ jobs:
|
||||
# pytest-benchmark --storage file://benchmarks/results --benchmark-compare
|
||||
|
||||
- name: Upload Benchmark Results
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: benchmark-report-${{ github.run_id }}
|
||||
|
||||
@@ -3,8 +3,18 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '30 1 * * 1' # Every Monday 7 AM IST
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze Python
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: python
|
||||
queries: security-and-quality
|
||||
config-file: .github/codeql/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:python"
|
||||
upload: false
|
||||
id: codeql
|
||||
|
||||
- name: Upload SARIF (Advanced Setup only)
|
||||
# Uploads results only when Default Setup is not active.
|
||||
# If Default Setup is still enabled, this step skips gracefully
|
||||
# instead of failing the workflow with HTTP 409.
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: ${{ steps.codeql.outputs.sarif-output }}
|
||||
category: "/language:python"
|
||||
wait-for-processing: true
|
||||
continue-on-error: true
|
||||
|
||||
# NOTE: Auto-dismissal by rule-id is intentionally removed.
|
||||
# Dismissing every alert that matches a rule ID would silently suppress
|
||||
# future real vulnerabilities of the same type. The alerts below were
|
||||
# individually triaged and dismissed manually in the security-enhancement
|
||||
# PR (alerts #12–#18). New alerts must be reviewed and dismissed by hand,
|
||||
# or will auto-close when the underlying code no longer triggers them.
|
||||
#
|
||||
# If you need to dismiss a specific known-safe alert, pin its alert NUMBER
|
||||
# here and remove it once CodeQL stops reporting it naturally. Example:
|
||||
#
|
||||
# PINNED_ALERT_NUMBERS=(12 13 14 15 16 17 18)
|
||||
# for NUM in "${PINNED_ALERT_NUMBERS[@]}"; do
|
||||
# gh api repos/$REPO/code-scanning/alerts/$NUM \
|
||||
# -X PATCH -f state=dismissed -f dismissed_reason="false positive" \
|
||||
# -f dismissed_comment="<reason>"
|
||||
# done
|
||||
@@ -8,11 +8,12 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'semantica/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- 'CHANGELOG.md'
|
||||
- 'RELEASE.md'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
# Permissions needed to deploy to GitHub Pages
|
||||
@@ -58,11 +59,11 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
uses: actions/configure-pages@v6
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: ./site
|
||||
|
||||
@@ -76,4 +77,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- run: pip install build
|
||||
- run: python -m build
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: dist/*
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
@@ -4,9 +4,19 @@ on:
|
||||
schedule:
|
||||
- cron: '30 1 * * 1,4' # Mon/Thu 7 AM IST
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
@@ -86,7 +96,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Security Reports
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: security-reports
|
||||
path: |
|
||||
@@ -96,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Comment PR with Security Results
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -158,7 +168,7 @@ jobs:
|
||||
}
|
||||
|
||||
// Create summary comment
|
||||
const comment = `# 🔒 Security Scan Results\\n\\n${safetyResults}\\n\\n${banditResults}\\n\\n${semgrepResults}\\n\\n---\\n\\n*This security scan runs automatically on every PR and bi-weekly.*\\n\\n📊 **Security Policy**: CI fails on vulnerabilities and HIGH severity issues.`;
|
||||
const comment = `# 🔒 Security Scan Results\\n\\n${safetyResults}\\n\\n${banditResults}\\n\\n${semgrepResults}\\n\\n---\\n\\n*This security scan runs automatically on source-code PRs and bi-weekly (skipped for doc/markdown-only changes).*\\n\\n📊 **Security Policy**: CI fails on vulnerabilities and HIGH severity issues.`;
|
||||
|
||||
// Post comment with error handling
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+10
@@ -110,3 +110,13 @@ sample_data/
|
||||
|
||||
# Test Results
|
||||
test_results.txt
|
||||
|
||||
# Frontend workspace artifacts
|
||||
semantica-explorer/
|
||||
node_modules/
|
||||
|
||||
# Frontend build artifacts (generated by Vite — do not track in git)
|
||||
semantica/static/
|
||||
|
||||
# Local graph explorer test datasets
|
||||
demo_out/
|
||||
|
||||
+472
-1571
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -2,9 +2,9 @@
|
||||
|
||||
Thank you for your interest in contributing! Every contribution, no matter how small, is valuable. 🎉
|
||||
|
||||
⭐ **Give us a Star** • 🍴 **[Fork Semantica](https://github.com/Hawksight-AI/semantica/fork)** • 💬 **Join our [Discord](https://discord.gg/N7WmAuDH)**
|
||||
⭐ **Give us a Star** • 🍴 **[Fork Semantica](https://github.com/Hawksight-AI/semantica/fork)** • 💬 **Join our [Discord](https://discord.gg/sV34vps5hH)**
|
||||
|
||||
> **New to contributing?** Start with a [`good first issue`](https://github.com/Hawksight-AI/semantica/labels/good%20first%20issue) or join our [Discord](https://discord.gg/N7WmAuDH) community.
|
||||
> **New to contributing?** Start with a [`good first issue`](https://github.com/Hawksight-AI/semantica/labels/good%20first%20issue) or join our [Discord](https://discord.gg/sV34vps5hH) community.
|
||||
|
||||
---
|
||||
|
||||
@@ -15,7 +15,7 @@ Thank you for your interest in contributing! Every contribution, no matter how s
|
||||
3. Make your changes
|
||||
4. Submit a pull request!
|
||||
|
||||
**Need help?** Join [Discord](https://discord.gg/N7WmAuDH) or [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
|
||||
**Need help?** Join [Discord](https://discord.gg/sV34vps5hH) or [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
|
||||
|
||||
---
|
||||
|
||||
@@ -108,7 +108,7 @@ Thank you for your interest in contributing! Every contribution, no matter how s
|
||||
|
||||
**What:** Help others in the community
|
||||
|
||||
**Where:** [Discord](https://discord.gg/N7WmAuDH), [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
|
||||
**Where:** [Discord](https://discord.gg/sV34vps5hH), [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
|
||||
|
||||
**Examples:** Answer questions, review PRs, share your projects
|
||||
|
||||
@@ -326,7 +326,7 @@ result = instance.method()
|
||||
|
||||
## 🆘 Getting Help
|
||||
|
||||
- 💬 [Discord](https://discord.gg/N7WmAuDH) - Real-time chat
|
||||
- 💬 [Discord](https://discord.gg/sV34vps5hH) - Real-time chat
|
||||
- 💭 [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions) - Q&A
|
||||
- 🐛 [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues) - Bug reports
|
||||
|
||||
@@ -363,4 +363,4 @@ This project follows a [Code of Conduct](CODE_OF_CONDUCT.md). Be respectful and
|
||||
|
||||
Every contribution matters - whether it's a single line of code, a typo fix, a helpful answer, or a bug report. We appreciate you! 🙏
|
||||
|
||||
⭐ **Give us a Star** • 🍴 **[Fork Semantica](https://github.com/Hawksight-AI/semantica/fork)** • 💬 **Join our [Discord](https://discord.gg/N7WmAuDH)**
|
||||
⭐ **Give us a Star** • 🍴 **[Fork Semantica](https://github.com/Hawksight-AI/semantica/fork)** • 💬 **Join our [Discord](https://discord.gg/sV34vps5hH)**
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ Thank you to all the people who have contributed to Semantica! 🎉
|
||||
|
||||
This project follows the [all-contributors](https://allcontributors.org) specification. Contributions of any kind are welcome!
|
||||
|
||||
⭐ **Give us a Star** • 🍴 **Fork us** • 💬 **Join our [Discord](https://discord.gg/N7WmAuDH)**
|
||||
⭐ **Give us a Star** • 🍴 **Fork us** • 💬 **Join our [Discord](https://discord.gg/sV34vps5hH)**
|
||||
|
||||
---
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
FROM node:25-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app/semantica-explorer
|
||||
|
||||
|
||||
COPY semantica-explorer/package.json semantica-explorer/package-lock.json* ./
|
||||
|
||||
|
||||
RUN npm install
|
||||
|
||||
|
||||
COPY semantica-explorer/ ./
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM python:3.14-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml ./
|
||||
COPY semantica/ ./semantica/
|
||||
|
||||
COPY --from=frontend-builder /app/semantica/static ./semantica/static
|
||||
|
||||
RUN pip install --no-cache-dir ".[explorer]"
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["python", "-m", "uvicorn", "semantica.explorer.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -1,105 +0,0 @@
|
||||
# Deduplication & Conflict Resolution Strategies Summary
|
||||
|
||||
## Quick Reference by Use Case
|
||||
|
||||
| Use Case | Deduplication Method | Merge Strategy | Conflict Detection | Conflict Resolution |
|
||||
|----------|---------------------|----------------|-------------------|---------------------|
|
||||
| **Finance** |
|
||||
| `01_Financial_Data_Integration_MCP` | `DuplicateDetector` (incremental) | `keep_highest_confidence` | `temporal` | `most_recent` |
|
||||
| `02_Fraud_Detection` | `ClusterBuilder` (graph_based) | `merge_all` | `logical` | `expert_review` |
|
||||
| **Biomedical** |
|
||||
| `01_Drug_Discovery_Pipeline` | `EntityResolver` (semantic) | - | `relationship` | `voting` |
|
||||
| `02_Genomic_Variant_Analysis` | `DuplicateDetector` (group) | `keep_most_complete` | `value` | `credibility_weighted` |
|
||||
| **Cybersecurity** |
|
||||
| `01_Real_Time_Anomaly_Detection` | `DuplicateDetector` (pairwise) | `keep_first` | `entity` | `first_seen` |
|
||||
| `02_Threat_Intelligence_Hybrid_RAG` | `EntityResolver` (exact) | - | `type` | `highest_confidence` |
|
||||
| **Blockchain** |
|
||||
| `01_DeFi_Protocol_Intelligence` | `DuplicateDetector` (group) | `keep_last` | `relationship` | `voting` |
|
||||
| `02_Transaction_Network_Analysis` | `ClusterBuilder` (hierarchical) | `keep_most_complete` | `temporal` | `most_recent` |
|
||||
| **Intelligence** |
|
||||
| `01_Criminal_Network_Analysis` | `EntityResolver` (fuzzy) | - | `value` | `credibility_weighted` |
|
||||
| `02_Intelligence_Analysis_Orchestrator_Worker` | `DuplicateDetector` (batch) | `merge_all` | `entity` | `voting` |
|
||||
| **Renewable Energy** |
|
||||
| `01_Energy_Market_Analysis` | `DuplicateDetector` (pairwise) | `keep_highest_confidence` | `temporal` | `most_recent` |
|
||||
| **Supply Chain** |
|
||||
| `01_Supply_Chain_Data_Integration` | `DuplicateDetector` (incremental) | `keep_most_complete` | `value` | `credibility_weighted` |
|
||||
|
||||
---
|
||||
|
||||
## Strategy Rationale by Domain
|
||||
|
||||
### Finance
|
||||
- **Financial Data Integration**: Incremental for streaming data; most_recent for time-sensitive financial data
|
||||
- **Fraud Detection**: Graph-based clustering for fraud groups; expert_review for fraud assessment
|
||||
|
||||
### Biomedical
|
||||
- **Drug Discovery**: Semantic matching for drug compounds; voting for research source aggregation
|
||||
- **Genomic Variants**: Group method for related variants; credibility weighting for research sources
|
||||
|
||||
### Cybersecurity
|
||||
- **Real-Time Anomaly**: Pairwise for real-time streams; keep_first for first detection priority
|
||||
- **Threat Intelligence**: Exact matching for IOCs; highest_confidence for threat classification
|
||||
|
||||
### Blockchain
|
||||
- **DeFi Protocols**: Group method for related protocols; keep_last for latest protocol info
|
||||
- **Transaction Networks**: Hierarchical clustering for nested groups; temporal for time-sensitive data
|
||||
|
||||
### Intelligence
|
||||
- **Criminal Networks**: Fuzzy matching for intelligence data; credibility weighting for intelligence sources
|
||||
- **Intelligence Analysis**: Batch for multi-source integration; merge_all to combine all intelligence sources
|
||||
|
||||
### Renewable Energy
|
||||
- **Energy Markets**: Pairwise for real-time market data; most_recent for time-sensitive energy data
|
||||
|
||||
### Supply Chain
|
||||
- **Supply Chain Integration**: Incremental for continuous updates; credibility weighting for supply chain sources
|
||||
|
||||
---
|
||||
|
||||
## Method Distribution
|
||||
|
||||
### Deduplication Methods (9 total)
|
||||
- `pairwise`: 2 notebooks (real-time processing)
|
||||
- `batch`: 3 notebooks (large datasets)
|
||||
- `incremental`: 2 notebooks (streaming/continuous)
|
||||
- `group`: 2 notebooks (related entities)
|
||||
- `graph_based` (ClusterBuilder): 2 notebooks (interconnected entities)
|
||||
- `hierarchical` (ClusterBuilder): 1 notebook (nested groups)
|
||||
- `exact` (EntityResolver): 1 notebook (exact matching)
|
||||
- `semantic` (EntityResolver): 2 notebooks (semantic similarity)
|
||||
- `fuzzy` (EntityResolver): 1 notebook (fuzzy matching)
|
||||
|
||||
### Merge Strategies (5 total)
|
||||
- `keep_first`: 1 notebook (first detection priority)
|
||||
- `keep_last`: 1 notebook (latest information)
|
||||
- `keep_most_complete`: 5 notebooks (preserve all details)
|
||||
- `keep_highest_confidence`: 2 notebooks (most reliable data)
|
||||
- `merge_all`: 3 notebooks (combine all information)
|
||||
|
||||
### Conflict Detection Methods (6 total)
|
||||
- `value`: 4 notebooks (property value conflicts)
|
||||
- `type`: 2 notebooks (type/classification conflicts)
|
||||
- `entity`: 2 notebooks (entity-wide conflicts)
|
||||
- `relationship`: 3 notebooks (relationship conflicts)
|
||||
- `temporal`: 3 notebooks (time-sensitive conflicts)
|
||||
- `logical`: 2 notebooks (logical inconsistencies)
|
||||
|
||||
### Conflict Resolution Strategies (6 total)
|
||||
- `voting`: 5 notebooks (majority vote)
|
||||
- `credibility_weighted`: 4 notebooks (source credibility)
|
||||
- `most_recent`: 3 notebooks (latest data)
|
||||
- `first_seen`: 1 notebook (first detection)
|
||||
- `highest_confidence`: 2 notebooks (most confident)
|
||||
- `expert_review`: 1 notebook (manual review)
|
||||
|
||||
---
|
||||
|
||||
## Key Patterns
|
||||
|
||||
1. **Real-Time Systems**: Use `pairwise` + `keep_first` + `first_seen`
|
||||
2. **Time-Sensitive Data**: Use `temporal` + `most_recent`
|
||||
3. **Multi-Source Integration**: Use `batch` + `merge_all` + `voting`
|
||||
4. **Medical/Research**: Use `credibility_weighted` for authoritative sources
|
||||
5. **Fraud/Security**: Use `graph_based` + `logical` + `expert_review`
|
||||
6. **Exact Matching Required**: Use `exact` strategy (IOCs, identifiers)
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ Start with our comprehensive documentation:
|
||||
|
||||
**Best for**: Real-time chat and quick questions
|
||||
|
||||
- [Join Discord](https://discord.gg/N7WmAuDH)
|
||||
- [Join Discord](https://discord.gg/sV34vps5hH)
|
||||
|
||||
#### GitHub Issues
|
||||
|
||||
|
||||
@@ -110,6 +110,35 @@ def generate_entity_cluster(base_name: str, size: int) -> List[Dict[str, Any]]:
|
||||
return entities
|
||||
|
||||
|
||||
def generate_relationship_dataset(size: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generates a dataset of graph relationships/triplets.
|
||||
Includes exact matches, synonym predicates, and dirty literal strings.
|
||||
"""
|
||||
relationships = []
|
||||
predicates = ["works_for", "employed_by", "is_employee_of", "has_employer"]
|
||||
|
||||
for i in range(size):
|
||||
# Base relationship
|
||||
rel = {
|
||||
"subject": f"Person_{i % 50}",
|
||||
"predicate": random.choice(predicates),
|
||||
"object": f"Company_{i % 10}"
|
||||
}
|
||||
relationships.append(rel)
|
||||
|
||||
# Inject semantic duplicates (dirty literals / synonym predicates)
|
||||
if random.random() < 0.4:
|
||||
dirty_rel = {
|
||||
"subject": f"Person_{i % 50}",
|
||||
"predicate": random.choice(predicates),
|
||||
"object": f" Company_{i % 10} Inc. "
|
||||
}
|
||||
relationships.append(dirty_rel)
|
||||
|
||||
return relationships
|
||||
|
||||
|
||||
def generate_dataset(
|
||||
num_clusters: int, items_per_cluster: int, worst_case_blocking: bool = False
|
||||
):
|
||||
@@ -187,12 +216,25 @@ def test_full_similarity_calculation(benchmark):
|
||||
def test_duplicate_detection_scaling_opt(benchmark, dataset_size):
|
||||
"""
|
||||
Tests duplication on a 'Distributed' dataset (Best Case)
|
||||
Now utilizing V2 Candidate Generation to ensure no regressions.
|
||||
"""
|
||||
|
||||
data = generate_dataset(
|
||||
num_clusters=dataset_size // 10, items_per_cluster=10, worst_case_blocking=False
|
||||
)
|
||||
detector = DuplicateDetector(similarity_threshold=0.8)
|
||||
|
||||
detector = DuplicateDetector(
|
||||
similarity_threshold=0.8,
|
||||
similarity={
|
||||
"candidate_strategy": "blocking_v2",
|
||||
"max_candidates_per_entity": 50,
|
||||
"prefilter_enabled": True,
|
||||
"score_breakdown_enabled": True,
|
||||
"prefilter_thresholds": {
|
||||
"min_length_ratio": 0.4,
|
||||
"require_shared_token": True
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
benchmark.pedantic(lambda: detector.detect_duplicates(data), iterations=1, rounds=5)
|
||||
|
||||
@@ -201,12 +243,25 @@ def test_duplicate_detection_scaling_opt(benchmark, dataset_size):
|
||||
def test_duplicate_detection_worst_Case(benchmark, dataset_size):
|
||||
"""
|
||||
Tests detection on a 'Clustered' dataset (Worst Case).
|
||||
Now utilizing V2 Candidate Generation to cut the pair explosion.
|
||||
"""
|
||||
|
||||
data = generate_dataset(
|
||||
num_clusters=dataset_size // 10, items_per_cluster=10, worst_case_blocking=True
|
||||
)
|
||||
detector = DuplicateDetector(similarity_threshold=0.8)
|
||||
|
||||
detector = DuplicateDetector(
|
||||
similarity_threshold=0.8,
|
||||
similarity={
|
||||
"candidate_strategy": "blocking_v2",
|
||||
"max_candidates_per_entity": 50,
|
||||
"prefilter_enabled": True,
|
||||
"score_breakdown_enabled": True,
|
||||
"prefilter_thresholds": {
|
||||
"min_length_ratio": 0.4,
|
||||
"require_shared_token": True
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
benchmark.pedantic(lambda: detector.detect_duplicates(data), iterations=1, rounds=5)
|
||||
|
||||
@@ -253,3 +308,31 @@ def test_merge_entity_benchmark(benchmark):
|
||||
iterations=10,
|
||||
rounds=10,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "semantic_v2"])
|
||||
def test_relationship_dedup_speed(benchmark, mode):
|
||||
"""
|
||||
Measures the speed of relationship/triplet deduplication.
|
||||
Compares the O(N^2) legacy fallback vs the fast canonical hash path.
|
||||
"""
|
||||
# Yields ~280 relationships (approx 39,000 comparisons in O(N^2))
|
||||
relationships = generate_relationship_dataset(200)
|
||||
|
||||
detector = DuplicateDetector()
|
||||
options = {
|
||||
"threshold": 0.85,
|
||||
"relationship_dedup_mode": mode,
|
||||
"predicate_synonym_map": {
|
||||
"works_for": "employed_by",
|
||||
"is_employee_of": "employed_by",
|
||||
"has_employer": "employed_by"
|
||||
},
|
||||
"literal_normalization_enabled": True
|
||||
}
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: detector.detect_relationship_duplicates(relationships, **options),
|
||||
iterations=5,
|
||||
rounds=10,
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
# Benchmark Tools
|
||||
|
||||
pytest>=7.0.0
|
||||
pytest-benchmark>=4.0.0
|
||||
pytest-benchmark>=5.2.3
|
||||
|
||||
# Core Utils
|
||||
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
{
|
||||
"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": [
|
||||
"[](https://colab.research.google.com/github/Hawksight-AI/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)\")"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"[](https://colab.research.google.com/github/Hawksight-AI/semantica/blob/main/cookbook/advanced/14_Datalog_Style_Reasoning.ipynb)\n",
|
||||
"\n",
|
||||
"# Datalog-Style Reasoning\n",
|
||||
"\n",
|
||||
"End-to-end guide to Semantica's **`DatalogReasoner`** — a native bottom-up semi-naive fixpoint engine — wired together with `GraphBuilder`, `ContextGraph`, `GraphAnalyzer`, `ExplanationGenerator`, and the supporting data-classes (`DatalogFact`, `DatalogRule`, `InferenceResult`, `Rule`).\n",
|
||||
"\n",
|
||||
"## What you will build\n",
|
||||
"\n",
|
||||
"| Part | Topic | Key classes |\n",
|
||||
"|------|-------|-------------|\n",
|
||||
"| 1 | Core API & EDB/IDB concepts | `DatalogReasoner`, `DatalogFact`, `DatalogRule` |\n",
|
||||
"| 2 | KG → Datalog pipeline | `GraphBuilder`, `GraphAnalyzer`, `DatalogReasoner` |\n",
|
||||
"| 3 | ContextGraph integration | `ContextGraph`, `DatalogReasoner.load_from_graph()` |\n",
|
||||
"| 4 | RBAC access-control policy | `GraphBuilder`, `DatalogReasoner`, `ExplanationGenerator` |\n",
|
||||
"| 5 | Org hierarchy | `ContextGraph`, `DatalogReasoner`, `InferenceResult` |\n",
|
||||
"| 6 | Engine introspection | `DatalogFact`, `DatalogRule` internal state |\n",
|
||||
"\n",
|
||||
"**Related notebooks**\n",
|
||||
"- [08_Reasoning_and_Inference.ipynb](08_Reasoning_and_Inference.ipynb) — high-level `Reasoner` with IF/THEN syntax\n",
|
||||
"- [10_Temporal_Knowledge_Graphs.ipynb](10_Temporal_Knowledge_Graphs.ipynb) — temporal reasoning\n",
|
||||
"\n",
|
||||
"**Documentation**: [Reasoning API](https://semantica.readthedocs.io/reference/reasoning/) | [KG API](https://semantica.readthedocs.io/reference/kg/) | [Context API](https://semantica.readthedocs.io/reference/context/)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install -qU semantica"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Reasoning ──────────────────────────────────────────────────────────────\n",
|
||||
"from semantica.reasoning import (\n",
|
||||
" DatalogReasoner, # native Datalog fixpoint engine\n",
|
||||
" DatalogFact, # frozen dataclass: predicate + args tuple\n",
|
||||
" DatalogRule, # dataclass: head + body (list[BodyAtom])\n",
|
||||
" ExplanationGenerator, # generates NL justifications\n",
|
||||
" InferenceResult, # result dataclass consumed by ExplanationGenerator\n",
|
||||
" Rule, # rule dataclass used by ExplanationGenerator\n",
|
||||
" RuleType, # enum: IMPLICATION | EQUIVALENCE | CONSTRAINT | TRANSFORMATION\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# ── Knowledge Graph ────────────────────────────────────────────────────────\n",
|
||||
"from semantica.kg import (\n",
|
||||
" GraphBuilder, # constructs KG dicts from entity+relationship sources\n",
|
||||
" GraphAnalyzer, # centrality, communities, connectivity, metrics\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# ── Context ────────────────────────────────────────────────────────────────\n",
|
||||
"from semantica.context import ContextGraph # in-memory graph: add_node/add_edge/find_*\n",
|
||||
"\n",
|
||||
"print(\"All Semantica classes imported successfully.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 1 — Core API: EDB Facts, IDB Rules, Fixpoint\n",
|
||||
"\n",
|
||||
"### Datalog in 30 seconds\n",
|
||||
"\n",
|
||||
"| Term | Meaning | Example |\n",
|
||||
"|------|---------|--------|\n",
|
||||
"| EDB (Extensional DB) | Ground facts you assert | `parent(tom, bob)` |\n",
|
||||
"| IDB (Intensional DB) | Facts derived by rules | `ancestor(tom, ann)` |\n",
|
||||
"| Rule (Horn clause) | If body → derive head | `ancestor(X,Y) :- parent(X,Y).` |\n",
|
||||
"| Variable | Uppercase, unified during eval | `X`, `Y`, `Role` |\n",
|
||||
"| Constant | Lowercase, matches literally | `tom`, `admin` |\n",
|
||||
"| Fixpoint | Iterate until no new facts appear | `DatalogReasoner.derive_all()` |\n",
|
||||
"\n",
|
||||
"### The canonical example — transitive ancestry"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 1: create engine ──────────────────────────────────────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"\n",
|
||||
"# ── Step 2: load EDB (ground facts) ───────────────────────────────────────\n",
|
||||
"# Syntax: predicate(constant1, constant2) — constants must be lowercase\n",
|
||||
"edb_facts = [\n",
|
||||
" \"parent(tom, bob)\",\n",
|
||||
" \"parent(bob, ann)\",\n",
|
||||
" \"parent(ann, pat)\",\n",
|
||||
"]\n",
|
||||
"for f in edb_facts:\n",
|
||||
" dr.add_fact(f)\n",
|
||||
"\n",
|
||||
"print(f\"EDB loaded: {len(dr._all_facts)} ground facts\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 3: add IDB rules (Horn clauses) ──────────────────────────────────\n",
|
||||
"# Syntax: head(Vars) :- body_atom1(Vars), body_atom2(Vars).\n",
|
||||
"# Variables start with uppercase; trailing '.' is optional\n",
|
||||
"dr.add_rule(\"ancestor(X, Y) :- parent(X, Y).\")\n",
|
||||
"dr.add_rule(\"ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).\") # recursive\n",
|
||||
"\n",
|
||||
"print(f\"Rules loaded: {len(dr._rules)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 4: fixpoint evaluation ────────────────────────────────────────────\n",
|
||||
"# derive_all() runs semi-naive bottom-up evaluation until no new facts appear\n",
|
||||
"all_facts: list[str] = dr.derive_all()\n",
|
||||
"\n",
|
||||
"ancestor_strs = sorted(f for f in all_facts if f.startswith(\"ancestor\"))\n",
|
||||
"print(f\"Derived {len(ancestor_strs)} ancestor facts:\")\n",
|
||||
"for f in ancestor_strs:\n",
|
||||
" print(\" \", f)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Step 5: query ──────────────────────────────────────────────────────────\n",
|
||||
"# Use '?varname' placeholders — query() auto-calls derive_all() if needed\n",
|
||||
"# Returns: list[dict] e.g. [{\"Y\": \"bob\"}, {\"Y\": \"ann\"}, {\"Y\": \"pat\"}]\n",
|
||||
"\n",
|
||||
"descendants = dr.query(\"ancestor(tom, ?Y)\")\n",
|
||||
"print(\"All descendants of tom:\", sorted(r[\"Y\"] for r in descendants))\n",
|
||||
"\n",
|
||||
"ancestors_of_pat = dr.query(\"ancestor(?X, pat)\")\n",
|
||||
"print(\"All ancestors of pat: \", sorted(r[\"X\"] for r in ancestors_of_pat))\n",
|
||||
"\n",
|
||||
"all_pairs = dr.query(\"ancestor(?X, ?Y)\")\n",
|
||||
"print(f\"\\nAll ancestor pairs ({len(all_pairs)}):\")\n",
|
||||
"for row in sorted(all_pairs, key=lambda r: (r[\"X\"], r[\"Y\"])):\n",
|
||||
" print(f\" {row['X']:6s} → {row['Y']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 2 — GraphBuilder → DatalogReasoner Pipeline\n",
|
||||
"\n",
|
||||
"`GraphBuilder` constructs a structured `{\"entities\": [...], \"relationships\": [...]}` dict from your data. We then:\n",
|
||||
"\n",
|
||||
"1. Analyse the graph with `GraphAnalyzer` to understand structure.\n",
|
||||
"2. Feed `kg[\"relationships\"]` into `DatalogReasoner` as EDB facts.\n",
|
||||
"3. Apply recursive Datalog rules over the KG."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build a software-dependency KG ────────────────────────────────────────\n",
|
||||
"entities = [\n",
|
||||
" {\"id\": \"pythonsdk\", \"name\": \"Python SDK\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"restapi\", \"name\": \"REST API\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"authservice\", \"name\": \"Auth Service\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"database\", \"name\": \"Database\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"dashboard\", \"name\": \"Dashboard\", \"type\": \"Component\"},\n",
|
||||
" {\"id\": \"analytics\", \"name\": \"Analytics\", \"type\": \"Component\"},\n",
|
||||
"]\n",
|
||||
"relationships = [\n",
|
||||
" {\"source\": \"pythonsdk\", \"target\": \"restapi\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"restapi\", \"target\": \"authservice\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"authservice\", \"target\": \"database\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"dashboard\", \"target\": \"restapi\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"dashboard\", \"target\": \"analytics\", \"type\": \"depends_on\"},\n",
|
||||
" {\"source\": \"analytics\", \"target\": \"database\", \"type\": \"depends_on\"},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# GraphBuilder validates, deduplicates, and packages the data\n",
|
||||
"builder = GraphBuilder(merge_entities=True, resolve_conflicts=False)\n",
|
||||
"kg = builder.build([{\"entities\": entities, \"relationships\": relationships}])\n",
|
||||
"\n",
|
||||
"print(f\"KG built — entities: {len(kg['entities'])}, relationships: {len(kg['relationships'])}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Analyse the graph structure before reasoning ───────────────────────────\n",
|
||||
"# GraphAnalyzer provides centrality, communities, connectivity, and metrics\n",
|
||||
"analyzer = GraphAnalyzer()\n",
|
||||
"metrics = analyzer.compute_metrics(graph=kg)\n",
|
||||
"\n",
|
||||
"print(\"Graph structure:\")\n",
|
||||
"print(f\" Nodes : {metrics['num_nodes']}\")\n",
|
||||
"print(f\" Edges : {metrics['num_edges']}\")\n",
|
||||
"if \"density\" in metrics:\n",
|
||||
" print(f\" Density : {metrics['density']:.3f}\")\n",
|
||||
"if \"is_connected\" in metrics:\n",
|
||||
" print(f\" Connected : {metrics['is_connected']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Load KG relationships as EDB facts ────────────────────────────────────\n",
|
||||
"# GraphBuilder output dicts use the same source/target/type shape that\n",
|
||||
"# DatalogReasoner.add_fact() natively understands\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"\n",
|
||||
"for rel in kg[\"relationships\"]:\n",
|
||||
" dr.add_fact(rel) # dict path: {\"source\": ..., \"target\": ..., \"type\": ...}\n",
|
||||
"\n",
|
||||
"print(f\"EDB loaded: {len(dr._all_facts)} dependency facts\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Transitive dependency closure ─────────────────────────────────────────\n",
|
||||
"# 'depends_on' is the predicate name that add_fact inferred from 'type'\n",
|
||||
"dr.add_rule(\"transitive_dep(X, Y) :- depends_on(X, Y).\")\n",
|
||||
"dr.add_rule(\"transitive_dep(X, Y) :- depends_on(X, Z), transitive_dep(Z, Y).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()\n",
|
||||
"\n",
|
||||
"# Everything that transitively depends on the database\n",
|
||||
"db_deps = sorted(r[\"X\"] for r in dr.query(\"transitive_dep(?X, database)\"))\n",
|
||||
"print(\"Components that transitively depend on Database:\")\n",
|
||||
"for c in db_deps:\n",
|
||||
" print(\" \", c)\n",
|
||||
"\n",
|
||||
"# What does pythonsdk transitively depend on?\n",
|
||||
"sdk_chain = sorted(r[\"Y\"] for r in dr.query(\"transitive_dep(pythonsdk, ?Y)\"))\n",
|
||||
"print(f\"\\nPython SDK full dependency chain: {sdk_chain}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 3 — ContextGraph + `load_from_graph()`\n",
|
||||
"\n",
|
||||
"`DatalogReasoner.load_from_graph(graph)` accepts any `ContextGraph` directly: it calls `graph.find_edges()` and `graph.find_nodes()` and converts each result into EDB facts automatically."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build an in-memory ContextGraph ───────────────────────────────────────\n",
|
||||
"# ContextGraph.add_node / add_edge are the canonical way to build in-memory KGs\n",
|
||||
"cg = ContextGraph()\n",
|
||||
"\n",
|
||||
"# Nodes\n",
|
||||
"for person in [\"alice\", \"bob\", \"carol\", \"dave\", \"eve\"]:\n",
|
||||
" cg.add_node(person, node_type=\"person\", name=person.capitalize())\n",
|
||||
"\n",
|
||||
"# Directed \"follows\" edges\n",
|
||||
"for src, dst in [(\"alice\", \"bob\"), (\"bob\", \"carol\"), (\"carol\", \"dave\"), (\"alice\", \"eve\"), (\"eve\", \"carol\")]:\n",
|
||||
" cg.add_edge(src, dst, edge_type=\"follows\")\n",
|
||||
"\n",
|
||||
"# Verify the graph built correctly\n",
|
||||
"nodes = cg.find_nodes(node_type=\"person\")\n",
|
||||
"edges = cg.find_edges(edge_type=\"follows\")\n",
|
||||
"print(f\"ContextGraph — nodes: {len(nodes)}, edges: {len(edges)}\")\n",
|
||||
"print(\"Edges:\", [(e.get(\"source\", e.get(\"source_id\")), e.get(\"target\", e.get(\"target_id\"))) for e in edges])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── load_from_graph() ingests the ContextGraph directly ───────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"n_loaded = dr.load_from_graph(cg) # calls cg.find_edges() + cg.find_nodes() internally\n",
|
||||
"print(f\"Facts loaded from ContextGraph: {n_loaded}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Influence reach via transitive 'follows' ──────────────────────────────\n",
|
||||
"dr.add_rule(\"influence(X, Y) :- follows(X, Y).\")\n",
|
||||
"dr.add_rule(\"influence(X, Y) :- follows(X, Z), influence(Z, Y).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()\n",
|
||||
"\n",
|
||||
"# Who can alice reach?\n",
|
||||
"alice_reach = sorted(r[\"Y\"] for r in dr.query(\"influence(alice, ?Y)\"))\n",
|
||||
"print(f\"Alice's influence reach : {alice_reach}\")\n",
|
||||
"\n",
|
||||
"# Who can reach dave?\n",
|
||||
"reach_dave = sorted(r[\"X\"] for r in dr.query(\"influence(?X, dave)\"))\n",
|
||||
"print(f\"Who can influence dave : {reach_dave}\")\n",
|
||||
"\n",
|
||||
"# Full influence matrix\n",
|
||||
"all_influence = dr.query(\"influence(?X, ?Y)\")\n",
|
||||
"print(f\"\\nTotal influence pairs: {len(all_influence)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 4 — RBAC Access-Control Policy\n",
|
||||
"\n",
|
||||
"We model a role-based access-control (RBAC) system:\n",
|
||||
"\n",
|
||||
"1. Use `GraphBuilder` to build a structured KG of users, roles, and permissions.\n",
|
||||
"2. Load it into `DatalogReasoner` for policy inference.\n",
|
||||
"3. Use `ExplanationGenerator` to produce audit-ready NL justifications."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build RBAC graph with GraphBuilder ────────────────────────────────────\n",
|
||||
"rbac_entities = [\n",
|
||||
" # Users\n",
|
||||
" {\"id\": \"alice\", \"type\": \"User\", \"name\": \"Alice\"},\n",
|
||||
" {\"id\": \"bob\", \"type\": \"User\", \"name\": \"Bob\"},\n",
|
||||
" {\"id\": \"carol\", \"type\": \"User\", \"name\": \"Carol\"},\n",
|
||||
" {\"id\": \"dave\", \"type\": \"User\", \"name\": \"Dave\"},\n",
|
||||
" # Roles\n",
|
||||
" {\"id\": \"admin\", \"type\": \"Role\", \"name\": \"Administrator\"},\n",
|
||||
" {\"id\": \"editor\", \"type\": \"Role\", \"name\": \"Editor\"},\n",
|
||||
" {\"id\": \"viewer\", \"type\": \"Role\", \"name\": \"Viewer\"},\n",
|
||||
" # Permissions\n",
|
||||
" {\"id\": \"read\", \"type\": \"Permission\"},\n",
|
||||
" {\"id\": \"write\", \"type\": \"Permission\"},\n",
|
||||
" {\"id\": \"delete\", \"type\": \"Permission\"},\n",
|
||||
" {\"id\": \"manage_users\", \"type\": \"Permission\"},\n",
|
||||
"]\n",
|
||||
"rbac_relationships = [\n",
|
||||
" # User → Role assignments\n",
|
||||
" {\"source\": \"alice\", \"target\": \"admin\", \"type\": \"has_role\"},\n",
|
||||
" {\"source\": \"bob\", \"target\": \"editor\", \"type\": \"has_role\"},\n",
|
||||
" {\"source\": \"carol\", \"target\": \"viewer\", \"type\": \"has_role\"},\n",
|
||||
" {\"source\": \"dave\", \"target\": \"editor\", \"type\": \"has_role\"},\n",
|
||||
" # Role hierarchy (admin inherits from editor, editor from viewer)\n",
|
||||
" {\"source\": \"admin\", \"target\": \"editor\", \"type\": \"role_inherits\"},\n",
|
||||
" {\"source\": \"editor\", \"target\": \"viewer\", \"type\": \"role_inherits\"},\n",
|
||||
" # Role → Permission grants\n",
|
||||
" {\"source\": \"viewer\", \"target\": \"read\", \"type\": \"role_has_perm\"},\n",
|
||||
" {\"source\": \"editor\", \"target\": \"write\", \"type\": \"role_has_perm\"},\n",
|
||||
" {\"source\": \"admin\", \"target\": \"delete\", \"type\": \"role_has_perm\"},\n",
|
||||
" {\"source\": \"admin\", \"target\": \"manage_users\", \"type\": \"role_has_perm\"},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"builder = GraphBuilder(merge_entities=True, resolve_conflicts=False)\n",
|
||||
"rbac_kg = builder.build([{\"entities\": rbac_entities, \"relationships\": rbac_relationships}])\n",
|
||||
"\n",
|
||||
"print(f\"RBAC KG — entities: {len(rbac_kg['entities'])}, relationships: {len(rbac_kg['relationships'])}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Analyse RBAC graph structure ──────────────────────────────────────────\n",
|
||||
"analyzer = GraphAnalyzer()\n",
|
||||
"metrics = analyzer.compute_metrics(graph=rbac_kg)\n",
|
||||
"centrality = analyzer.calculate_centrality(rbac_kg, centrality_type=\"degree\")\n",
|
||||
"\n",
|
||||
"print(f\"RBAC graph — {metrics['num_nodes']} nodes, {metrics['num_edges']} edges\")\n",
|
||||
"if isinstance(centrality, dict) and \"degree\" in centrality:\n",
|
||||
" top = sorted(centrality[\"degree\"].items(), key=lambda x: x[1], reverse=True)[:3]\n",
|
||||
" print(\"Top-3 nodes by degree centrality:\", top)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Load RBAC KG into DatalogReasoner ────────────────────────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"\n",
|
||||
"for rel in rbac_kg[\"relationships\"]:\n",
|
||||
" dr.add_fact(rel) # {source, target, type} → predicate(source, target)\n",
|
||||
"\n",
|
||||
"# ── IDB rules: transitive role hierarchy ─────────────────────────────────\n",
|
||||
"dr.add_rule(\"effective_role(R, R2) :- role_inherits(R, R2).\")\n",
|
||||
"dr.add_rule(\"effective_role(R, R2) :- role_inherits(R, Z), effective_role(Z, R2).\")\n",
|
||||
"\n",
|
||||
"# ── IDB rules: inherited permissions ─────────────────────────────────────\n",
|
||||
"dr.add_rule(\"role_can(R, P) :- role_has_perm(R, P).\")\n",
|
||||
"dr.add_rule(\"role_can(R, P) :- effective_role(R, R2), role_has_perm(R2, P).\")\n",
|
||||
"\n",
|
||||
"# ── IDB rules: user effective permissions ────────────────────────────────\n",
|
||||
"dr.add_rule(\"can(U, P) :- has_role(U, R), role_can(R, P).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()\n",
|
||||
"\n",
|
||||
"print(\"User permissions derived via role-hierarchy inference:\")\n",
|
||||
"for user in [\"alice\", \"bob\", \"carol\", \"dave\"]:\n",
|
||||
" perms = sorted(r[\"P\"] for r in dr.query(f\"can({user}, ?P)\"))\n",
|
||||
" print(f\" {user:6s}: {perms}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── ExplanationGenerator — audit-ready NL justification ──────────────────\n",
|
||||
"# ExplanationGenerator works with InferenceResult objects.\n",
|
||||
"# We construct one manually to represent a derived Datalog conclusion.\n",
|
||||
"\n",
|
||||
"explainer = ExplanationGenerator(detail_level=\"detailed\")\n",
|
||||
"\n",
|
||||
"# Build the Rule object that represents the permission derivation chain\n",
|
||||
"perm_rule = Rule(\n",
|
||||
" rule_id=\"rbac_perm_chain\",\n",
|
||||
" name=\"RBAC permission via role hierarchy\",\n",
|
||||
" conditions=[\"has_role(alice, admin)\", \"effective_role(admin, viewer)\", \"role_has_perm(viewer, read)\"],\n",
|
||||
" conclusion=\"can(alice, read)\",\n",
|
||||
" rule_type=RuleType.IMPLICATION,\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Build InferenceResult representing the Datalog conclusion\n",
|
||||
"result = InferenceResult(\n",
|
||||
" conclusion=\"can(alice, read)\",\n",
|
||||
" rule_used=perm_rule,\n",
|
||||
" premises=[\n",
|
||||
" \"has_role(alice, admin)\",\n",
|
||||
" \"role_inherits(admin, editor)\",\n",
|
||||
" \"role_inherits(editor, viewer)\",\n",
|
||||
" \"role_has_perm(viewer, read)\",\n",
|
||||
" ],\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Generate NL explanation\n",
|
||||
"explanation = explainer.generate_explanation(result)\n",
|
||||
"print(\"Explanation type :\", explanation.explanation_type)\n",
|
||||
"print(\"Conclusion :\", explanation.conclusion)\n",
|
||||
"print(\"Natural language :\", explanation.natural_language)\n",
|
||||
"print(\"Reasoning steps :\", len(explanation.reasoning_path.steps))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Inverse queries ───────────────────────────────────────────────────────\n",
|
||||
"deleters = sorted(r[\"U\"] for r in dr.query(\"can(?U, delete)\"))\n",
|
||||
"print(\"Who can delete:\", deleters)\n",
|
||||
"\n",
|
||||
"writers = sorted(r[\"U\"] for r in dr.query(\"can(?U, write)\"))\n",
|
||||
"print(\"Who can write: \", writers)\n",
|
||||
"\n",
|
||||
"# All (user, permission) pairs — full policy matrix\n",
|
||||
"all_caps = dr.query(\"can(?U, ?P)\")\n",
|
||||
"print(f\"\\nTotal (user, permission) pairs: {len(all_caps)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 5 — Organisation Hierarchy with ContextGraph\n",
|
||||
"\n",
|
||||
"We model a company org-chart using `ContextGraph` and derive:\n",
|
||||
"- `manages(M, E)` — direct and transitive management\n",
|
||||
"- `skip_level(M, E)` — two hops up the chain\n",
|
||||
"- `same_team(X, Y)` — shared team membership"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── ContextGraph: org chart ────────────────────────────────────────────────\n",
|
||||
"org = ContextGraph()\n",
|
||||
"\n",
|
||||
"# Add employees as nodes with metadata\n",
|
||||
"staff = [\n",
|
||||
" (\"eng1\", \"engineer\", \"backend\"),\n",
|
||||
" (\"eng2\", \"engineer\", \"backend\"),\n",
|
||||
" (\"eng3\", \"engineer\", \"frontend\"),\n",
|
||||
" (\"techlead\", \"lead\", \"engineering\"),\n",
|
||||
" (\"design1\", \"designer\", \"ux\"),\n",
|
||||
" (\"design2\", \"designer\", \"ux\"),\n",
|
||||
" (\"designlead\",\"lead\", \"design\"),\n",
|
||||
" (\"vpeng\", \"vp\", \"engineering\"),\n",
|
||||
" (\"cto\", \"executive\", \"leadership\"),\n",
|
||||
"]\n",
|
||||
"for emp_id, role, team in staff:\n",
|
||||
" org.add_node(emp_id, node_type=\"employee\", role=role, team=team)\n",
|
||||
"\n",
|
||||
"# Reporting lines\n",
|
||||
"reports_to = [\n",
|
||||
" (\"eng1\", \"techlead\"), (\"eng2\", \"techlead\"), (\"eng3\", \"techlead\"),\n",
|
||||
" (\"techlead\", \"vpeng\"),\n",
|
||||
" (\"design1\", \"designlead\"), (\"design2\", \"designlead\"),\n",
|
||||
" (\"designlead\", \"vpeng\"),\n",
|
||||
" (\"vpeng\", \"cto\"),\n",
|
||||
"]\n",
|
||||
"for employee, manager in reports_to:\n",
|
||||
" org.add_edge(employee, manager, edge_type=\"reports_to\")\n",
|
||||
"\n",
|
||||
"# Team membership edges\n",
|
||||
"teams = [\n",
|
||||
" (\"eng1\", \"backend\"), (\"eng2\", \"backend\"), (\"eng3\", \"frontend\"),\n",
|
||||
" (\"design1\", \"ux\"), (\"design2\", \"ux\"),\n",
|
||||
"]\n",
|
||||
"for emp, team in teams:\n",
|
||||
" org.add_edge(emp, team, edge_type=\"in_team\")\n",
|
||||
" if not org.find_nodes(node_type=\"team\"):\n",
|
||||
" org.add_node(team, node_type=\"team\")\n",
|
||||
"\n",
|
||||
"print(f\"ContextGraph — nodes: {len(org.find_nodes())}, edges: {len(org.find_edges())}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Load org chart into DatalogReasoner ───────────────────────────────────\n",
|
||||
"dr = DatalogReasoner()\n",
|
||||
"n = dr.load_from_graph(org) # uses org.find_edges() + org.find_nodes()\n",
|
||||
"print(f\"Facts loaded via load_from_graph(): {n}\")\n",
|
||||
"\n",
|
||||
"# ── IDB rules ─────────────────────────────────────────────────────────────\n",
|
||||
"# Transitive management chain\n",
|
||||
"dr.add_rule(\"manages(M, E) :- reports_to(E, M).\")\n",
|
||||
"dr.add_rule(\"manages(M, E) :- reports_to(E, Z), manages(M, Z).\")\n",
|
||||
"\n",
|
||||
"# Skip-level: exactly two reporting hops\n",
|
||||
"dr.add_rule(\"skip_level(M, E) :- reports_to(E, Z), reports_to(Z, M).\")\n",
|
||||
"\n",
|
||||
"# Same team\n",
|
||||
"dr.add_rule(\"same_team(X, Y) :- in_team(X, T), in_team(Y, T).\")\n",
|
||||
"\n",
|
||||
"dr.derive_all()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Query org hierarchy ────────────────────────────────────────────────────\n",
|
||||
"# Everyone under CTO\n",
|
||||
"under_cto = sorted(r[\"E\"] for r in dr.query(\"manages(cto, ?E)\"))\n",
|
||||
"print(f\"CTO manages ({len(under_cto)} people): {under_cto}\")\n",
|
||||
"\n",
|
||||
"# VP Eng's direct + indirect reports\n",
|
||||
"under_vp = sorted(r[\"E\"] for r in dr.query(\"manages(vpeng, ?E)\"))\n",
|
||||
"print(f\"VP Eng manages : {under_vp}\")\n",
|
||||
"\n",
|
||||
"# Skip-level reports to CTO (people two hops below CTO)\n",
|
||||
"skip = sorted(r[\"E\"] for r in dr.query(\"skip_level(cto, ?E)\"))\n",
|
||||
"print(f\"CTO skip-level reports : {skip}\")\n",
|
||||
"\n",
|
||||
"# eng1's teammates\n",
|
||||
"mates = [r[\"Y\"] for r in dr.query(\"same_team(eng1, ?Y)\") if r[\"Y\"] != \"eng1\"]\n",
|
||||
"print(f\"eng1's teammates : {sorted(mates)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Build an InferenceResult and explain an org query ────────────────────\n",
|
||||
"explainer = ExplanationGenerator(detail_level=\"verbose\")\n",
|
||||
"\n",
|
||||
"mgmt_rule = Rule(\n",
|
||||
" rule_id=\"transitive_manages\",\n",
|
||||
" name=\"Transitive management chain\",\n",
|
||||
" conditions=[\"reports_to(eng1, techlead)\", \"manages(vpeng, techlead)\"],\n",
|
||||
" conclusion=\"manages(vpeng, eng1)\",\n",
|
||||
" rule_type=RuleType.IMPLICATION,\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"result = InferenceResult(\n",
|
||||
" conclusion=\"manages(vpeng, eng1)\",\n",
|
||||
" rule_used=mgmt_rule,\n",
|
||||
" premises=[\"reports_to(eng1, techlead)\", \"reports_to(techlead, vpeng)\"],\n",
|
||||
" confidence=1.0,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"exp = explainer.generate_explanation(result)\n",
|
||||
"print(exp.natural_language)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Part 6 — Engine Introspection: DatalogFact & DatalogRule\n",
|
||||
"\n",
|
||||
"After reasoning, the engine's internal state is fully accessible via `DatalogFact` and `DatalogRule` data-classes. Use this for auditing, debugging, or downstream export."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Inspect DatalogRule objects ────────────────────────────────────────────\n",
|
||||
"# dr._rules → List[DatalogRule]\n",
|
||||
"# DatalogRule.head_predicate, .head_args, .body (body = List[BodyAtom])\n",
|
||||
"print(\"Rules in engine:\")\n",
|
||||
"for rule in dr._rules:\n",
|
||||
" body_str = \", \".join(\n",
|
||||
" f\"{atom.predicate}({', '.join(atom.args)})\"\n",
|
||||
" for atom in rule.body\n",
|
||||
" )\n",
|
||||
" head_str = f\"{rule.head_predicate}({', '.join(rule.head_args)})\"\n",
|
||||
" print(f\" {head_str} :- {body_str}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Inspect DatalogFact objects ────────────────────────────────────────────\n",
|
||||
"# dr._all_facts → Set[DatalogFact] (EDB + IDB combined after derive_all)\n",
|
||||
"# dr._fact_index → Dict[predicate, Set[DatalogFact]]\n",
|
||||
"\n",
|
||||
"from collections import Counter\n",
|
||||
"\n",
|
||||
"# Count facts per predicate\n",
|
||||
"predicate_counts = Counter(f.predicate for f in dr._all_facts)\n",
|
||||
"print(\"Facts per predicate (EDB + derived IDB):\")\n",
|
||||
"for pred, count in sorted(predicate_counts.items()):\n",
|
||||
" print(f\" {pred:20s}: {count}\")\n",
|
||||
"print(f\"\\n TOTAL: {len(dr._all_facts)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Separate EDB from IDB ─────────────────────────────────────────────────\n",
|
||||
"# EDB predicates are the ones we added via add_fact (not derived by rules)\n",
|
||||
"idb_predicates = {rule.head_predicate for rule in dr._rules}\n",
|
||||
"edb_predicates = {f.predicate for f in dr._all_facts} - idb_predicates\n",
|
||||
"\n",
|
||||
"print(f\"EDB predicates (base facts) : {sorted(edb_predicates)}\")\n",
|
||||
"print(f\"IDB predicates (derived) : {sorted(idb_predicates)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Sample DatalogFact structure ──────────────────────────────────────────\n",
|
||||
"# DatalogFact is a frozen dataclass: predicate: str, args: Tuple[str, ...]\n",
|
||||
"manages_facts = sorted(dr._fact_index.get(\"manages\", []), key=lambda f: f.args)\n",
|
||||
"print(f\"First 5 'manages' DatalogFact objects ({len(manages_facts)} total):\")\n",
|
||||
"for fact in manages_facts[:5]:\n",
|
||||
" # Access predicate and args directly from the dataclass\n",
|
||||
" print(f\" DatalogFact(predicate={fact.predicate!r}, args={fact.args})\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── clear() resets the engine completely ─────────────────────────────────\n",
|
||||
"print(f\"Facts before clear(): {len(dr._all_facts)}\")\n",
|
||||
"dr.clear()\n",
|
||||
"print(f\"Facts after clear(): {len(dr._all_facts)}\")\n",
|
||||
"print(f\"Rules after clear(): {len(dr._rules)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## API Summary\n",
|
||||
"\n",
|
||||
"### DatalogReasoner\n",
|
||||
"\n",
|
||||
"| Method | Input | Output | Notes |\n",
|
||||
"|--------|-------|--------|-------|\n",
|
||||
"| `add_fact(f)` | `str` or `dict` | `None` | string: `\"pred(a, b)\"` · dict: `{source, target, type}` |\n",
|
||||
"| `add_rule(s)` | `str` | `None` | Horn clause: `\"head(X) :- body(X, Y).\"` |\n",
|
||||
"| `derive_all()` | — | `list[str]` | semi-naive fixpoint; idempotent |\n",
|
||||
"| `query(pat)` | `str` | `list[dict]` | `\"pred(a, ?Y)\"` → `[{\"Y\": ...}]` |\n",
|
||||
"| `load_from_graph(g)` | `ContextGraph` | `int` | facts loaded count |\n",
|
||||
"| `clear()` | — | `None` | resets engine |\n",
|
||||
"\n",
|
||||
"### Syntax rules\n",
|
||||
"\n",
|
||||
"| Item | Rule | Example |\n",
|
||||
"|------|------|---------|\n",
|
||||
"| Variable | Starts **uppercase** | `X`, `Role`, `Parent` |\n",
|
||||
"| Constant | All **lowercase** | `tom`, `admin`, `database` |\n",
|
||||
"| Query var | Prefix `?` | `?X`, `?Y`, `?Role` |\n",
|
||||
"| Rule body | `:-` separator, comma between atoms | `head(X) :- a(X, Z), b(Z, Y).` |\n",
|
||||
"\n",
|
||||
"### Class map\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"GraphBuilder.build() → kg dict {entities, relationships}\n",
|
||||
" ↓ kg[\"relationships\"] → dr.add_fact(rel)\n",
|
||||
" \n",
|
||||
"ContextGraph.add_node/add_edge → in-memory graph\n",
|
||||
" ↓ dr.load_from_graph(cg)\n",
|
||||
" \n",
|
||||
"DatalogReasoner.add_rule() → Horn clause rules\n",
|
||||
"DatalogReasoner.derive_all() → semi-naive fixpoint\n",
|
||||
"DatalogReasoner.query() → result rows\n",
|
||||
" ↓ build InferenceResult\n",
|
||||
" \n",
|
||||
"ExplanationGenerator → natural language justification\n",
|
||||
"GraphAnalyzer → graph structure metrics pre/post reasoning\n",
|
||||
"DatalogFact / DatalogRule → introspect engine state\n",
|
||||
"```"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "title",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Agno × Semantica: Decision Intelligence Agent\n",
|
||||
"\n",
|
||||
"This notebook shows how to wire Semantica's **Decision Intelligence** stack into an Agno agent so it can:\n",
|
||||
"\n",
|
||||
"- Record every decision it makes with full reasoning provenance\n",
|
||||
"- Search historical precedents before acting\n",
|
||||
"- Validate decisions against policy rules\n",
|
||||
"- Trace causal chains across decisions\n",
|
||||
"- Accumulate institutional knowledge that survives across sessions\n",
|
||||
"\n",
|
||||
"**Domain used:** Financial loan underwriting (easily adapted to healthcare, legal, HR, etc.)\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Architecture\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"Agno Agent\n",
|
||||
" ├── memory=AgnoContextStore ← graph-backed persistent memory\n",
|
||||
" └── tools=[AgnoDecisionKit] ← decision tools the LLM can call\n",
|
||||
" │\n",
|
||||
" ├── record_decision ← Semantica AgentContext.record_decision()\n",
|
||||
" ├── find_precedents ← Semantica AgentContext.find_precedents_advanced()\n",
|
||||
" ├── trace_causal_chain ← Semantica ContextGraph.trace_decision_causality()\n",
|
||||
" ├── analyze_impact ← Semantica AgentContext.analyze_decision_influence()\n",
|
||||
" ├── check_policy ← Semantica PolicyEngine\n",
|
||||
" └── get_decision_summary ← Semantica AgentContext.get_context_insights()\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"## Install\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"pip install semantica[agno]\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "setup-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Setup — Semantica Backends\n",
|
||||
"\n",
|
||||
"We build the Semantica components first. These are **independent of Agno** — you can swap backends without touching agent code."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "imports",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys, os\n",
|
||||
"sys.path.insert(0, os.path.abspath(\"../../\"))\n",
|
||||
"\n",
|
||||
"# ── Semantica core (not Agno-specific) ──────────────────────────────────────\n",
|
||||
"from semantica.context import AgentContext, ContextGraph\n",
|
||||
"from semantica.context import PolicyEngine, DecisionQuery, CausalChainAnalyzer\n",
|
||||
"from semantica.vector_store import VectorStore\n",
|
||||
"\n",
|
||||
"# ── Agno integration layer ───────────────────────────────────────────────────\n",
|
||||
"from integrations.agno import AgnoContextStore, AgnoDecisionKit, AGNO_AVAILABLE\n",
|
||||
"\n",
|
||||
"print(f\"Semantica imports OK\")\n",
|
||||
"print(f\"Agno installed: {AGNO_AVAILABLE}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "semantica-backends",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Vector store (FAISS, no external service needed) ────────────────────────\n",
|
||||
"vector_store = VectorStore(backend=\"faiss\", dimension=768)\n",
|
||||
"print(\"VectorStore ready (FAISS)\")\n",
|
||||
"\n",
|
||||
"# ── In-memory context graph with full analytics ──────────────────────────────\n",
|
||||
"knowledge_graph = ContextGraph(\n",
|
||||
" advanced_analytics=True,\n",
|
||||
" # Switch to neo4j for production:\n",
|
||||
" # backend=\"neo4j\", uri=\"bolt://localhost:7687\"\n",
|
||||
")\n",
|
||||
"print(\"ContextGraph ready (in-memory)\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "seed-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Seed Historical Decisions\n",
|
||||
"\n",
|
||||
"Before the agent runs, we pre-load historical decisions using **native Semantica APIs** so the precedent database is warm.\n",
|
||||
"\n",
|
||||
"In production you would ingest from a database or a prior session's graph export."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "seed-decisions",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Build a pure-Semantica AgentContext for seeding historical data\n",
|
||||
"seed_context = AgentContext(\n",
|
||||
" vector_store=vector_store,\n",
|
||||
" knowledge_graph=knowledge_graph,\n",
|
||||
" decision_tracking=True,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"historical_loans = [\n",
|
||||
" dict(\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
" scenario=\"Applicant: credit score 740, income $95k, DTI 28%, down payment 20%\",\n",
|
||||
" reasoning=\"Strong credit history, debt load well below 35% threshold, adequate down payment\",\n",
|
||||
" outcome=\"approved\",\n",
|
||||
" confidence=0.96,\n",
|
||||
" ),\n",
|
||||
" dict(\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
" scenario=\"Applicant: credit score 620, income $45k, DTI 42%, down payment 5%\",\n",
|
||||
" reasoning=\"Credit score below 650 floor, DTI exceeds 40% maximum, insufficient down payment\",\n",
|
||||
" outcome=\"rejected\",\n",
|
||||
" confidence=0.97,\n",
|
||||
" ),\n",
|
||||
" dict(\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
" scenario=\"Applicant: credit score 700, income $72k, DTI 33%, down payment 15%\",\n",
|
||||
" reasoning=\"Adequate credit, moderate DTI within range, down payment slightly below ideal\",\n",
|
||||
" outcome=\"approved_with_conditions\",\n",
|
||||
" confidence=0.82,\n",
|
||||
" ),\n",
|
||||
" dict(\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
" scenario=\"Applicant: credit score 780, income $130k, DTI 22%, down payment 30%\",\n",
|
||||
" reasoning=\"Excellent credit, low debt load, strong down payment — low-risk profile\",\n",
|
||||
" outcome=\"approved\",\n",
|
||||
" confidence=0.99,\n",
|
||||
" ),\n",
|
||||
" dict(\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
" scenario=\"Applicant: credit score 660, income $58k, DTI 38%, down payment 10%\",\n",
|
||||
" reasoning=\"Borderline credit, high DTI, minimal down payment — escalated to senior review\",\n",
|
||||
" outcome=\"escalated\",\n",
|
||||
" confidence=0.70,\n",
|
||||
" ),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for loan in historical_loans:\n",
|
||||
" did = seed_context.record_decision(**loan)\n",
|
||||
" print(f\" Seeded [{loan['outcome']:25s}] → {did}\")\n",
|
||||
"\n",
|
||||
"print(f\"\\n{len(historical_loans)} historical decisions loaded into Semantica KG\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "policy-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Define Policy Rules with Semantica\n",
|
||||
"\n",
|
||||
"We use `PolicyEngine` directly — no Agno involvement here. The `AgnoDecisionKit.check_policy` tool will call this engine during the agent's reasoning loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "policy",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"LENDING_POLICY_RULES = [\n",
|
||||
" \"credit_score >= 650\",\n",
|
||||
" \"dti <= 40\",\n",
|
||||
" \"down_payment_pct >= 10\",\n",
|
||||
" \"confidence >= 0.70\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# Verify directly with Semantica's PolicyEngine before wiring to Agno\n",
|
||||
"policy_engine = PolicyEngine(graph_store=knowledge_graph)\n",
|
||||
"\n",
|
||||
"test_application = {\"credit_score\": 720, \"dti\": 31, \"down_payment_pct\": 18, \"confidence\": 0.88}\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" result = policy_engine.check_compliance(test_application, LENDING_POLICY_RULES)\n",
|
||||
" print(f\"Policy check result: compliant={getattr(result, 'compliant', 'N/A')}\")\n",
|
||||
" print(f\"Violations: {getattr(result, 'violations', [])}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"PolicyEngine fallback (expected without full rule engine): {e}\")\n",
|
||||
"\n",
|
||||
"print(\"\\nPolicy rules defined:\", LENDING_POLICY_RULES)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "agent-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Build the Agno Decision-Intelligence Agent\n",
|
||||
"\n",
|
||||
"Now we wire everything into Agno using the integration classes.\n",
|
||||
"\n",
|
||||
"- `AgnoContextStore` gives the agent **persistent graph-backed memory**\n",
|
||||
"- `AgnoDecisionKit` exposes **6 decision tools** the LLM can invoke during reasoning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "build-agent",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── AgnoContextStore: wraps AgentContext as Agno MemoryDb ────────────────────\n",
|
||||
"store = AgnoContextStore(\n",
|
||||
" vector_store=vector_store, # Same store — shares seeded decisions\n",
|
||||
" knowledge_graph=knowledge_graph, # Same graph — shares seeded decisions\n",
|
||||
" decision_tracking=True,\n",
|
||||
" graph_expansion=True,\n",
|
||||
" session_id=\"loan_underwriter_v1\",\n",
|
||||
")\n",
|
||||
"print(\"AgnoContextStore ready\")\n",
|
||||
"\n",
|
||||
"# ── AgnoDecisionKit: exposes Semantica decision tools to Agno's LLM ──────────\n",
|
||||
"decision_kit = AgnoDecisionKit(\n",
|
||||
" context=store.context, # Reuse same AgentContext — shared decision history\n",
|
||||
" max_precedents=5,\n",
|
||||
" causal_depth=3,\n",
|
||||
" enable_policy_check=True,\n",
|
||||
")\n",
|
||||
"print(f\"AgnoDecisionKit ready — {len(decision_kit._tools)} tools registered\")\n",
|
||||
"print(\" Tools:\", [fn.__name__ for fn in decision_kit._tools])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "wire-agent",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if AGNO_AVAILABLE:\n",
|
||||
" from agno.agent import Agent\n",
|
||||
" from agno.memory import AgentMemory\n",
|
||||
" from agno.models.openai import OpenAIChat # or any Agno-supported model\n",
|
||||
"\n",
|
||||
" agent = Agent(\n",
|
||||
" name=\"LoanUnderwriter\",\n",
|
||||
" model=OpenAIChat(id=\"gpt-4o\"),\n",
|
||||
" memory=AgentMemory(db=store),\n",
|
||||
" tools=[decision_kit],\n",
|
||||
" show_tool_calls=True,\n",
|
||||
" description=(\n",
|
||||
" \"You are a senior loan underwriter. Before approving or rejecting any application:\"\n",
|
||||
" \" (1) find_precedents for similar past cases,\"\n",
|
||||
" \" (2) check_policy compliance,\"\n",
|
||||
" \" (3) record_decision with full reasoning.\"\n",
|
||||
" \" Always cite precedents and policy rule results in your explanation.\"\n",
|
||||
" ),\n",
|
||||
" )\n",
|
||||
" print(\"Agno Agent assembled and ready\")\n",
|
||||
"else:\n",
|
||||
" print(\"Agno not installed — demonstrating tool calls directly below\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "demo-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Demonstrate Decision Tools\n",
|
||||
"\n",
|
||||
"We call the decision tools **directly** so the notebook is fully runnable without an OpenAI key. When Agno is wired, the LLM orchestrates these same calls automatically."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-find-precedents",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"\n",
|
||||
"# ── 5a. Find Precedents ───────────────────────────────────────────────────────\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"print(\"TOOL: find_precedents\")\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"\n",
|
||||
"new_application_scenario = (\n",
|
||||
" \"Applicant: credit score 715, income $82k, DTI 30%, down payment 18%\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"precedents_json = decision_kit.find_precedents(\n",
|
||||
" scenario=new_application_scenario,\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
" limit=3,\n",
|
||||
")\n",
|
||||
"precedents = json.loads(precedents_json)\n",
|
||||
"print(f\"Found {precedents['count']} similar past decisions:\")\n",
|
||||
"for p in precedents['precedents']:\n",
|
||||
" print(f\" [{p.get('outcome','?'):25s}] confidence={p.get('confidence',0):.2f}\")\n",
|
||||
" print(f\" {p.get('scenario','')[:80]}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-policy",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── 5b. Check Policy ─────────────────────────────────────────────────────────\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"print(\"TOOL: check_policy\")\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"\n",
|
||||
"decision_data = json.dumps({\n",
|
||||
" \"credit_score\": 715,\n",
|
||||
" \"dti\": 30,\n",
|
||||
" \"down_payment_pct\": 18,\n",
|
||||
" \"confidence\": 0.88,\n",
|
||||
" \"outcome\": \"approved\",\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"policy_json = decision_kit.check_policy(\n",
|
||||
" decision_data=decision_data,\n",
|
||||
" policy_rules=json.dumps(LENDING_POLICY_RULES),\n",
|
||||
")\n",
|
||||
"policy_result = json.loads(policy_json)\n",
|
||||
"print(f\"Compliant: {policy_result.get('compliant')}\")\n",
|
||||
"print(f\"Violations: {policy_result.get('violations', [])}\")\n",
|
||||
"print(f\"Warnings: {policy_result.get('warnings', [])}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-record",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── 5c. Record Decision ──────────────────────────────────────────────────────\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"print(\"TOOL: record_decision\")\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"\n",
|
||||
"record_json = decision_kit.record_decision(\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
" scenario=new_application_scenario,\n",
|
||||
" reasoning=(\n",
|
||||
" \"3 similar precedents found — 2 approved, 1 escalated. \"\n",
|
||||
" \"Credit score 715 exceeds 650 floor. DTI 30% well within 40% limit. \"\n",
|
||||
" \"Down payment 18% above 10% minimum. All policy rules satisfied.\"\n",
|
||||
" ),\n",
|
||||
" outcome=\"approved\",\n",
|
||||
" confidence=0.91,\n",
|
||||
" entities=\"loan_applicant, credit_bureau, lending_policy_v2\",\n",
|
||||
")\n",
|
||||
"record_result = json.loads(record_json)\n",
|
||||
"decision_id = record_result['decision_id']\n",
|
||||
"print(f\"Decision recorded: {decision_id}\")\n",
|
||||
"print(f\"Status: {record_result['status']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-impact",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── 5d. Analyze Impact ───────────────────────────────────────────────────────\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"print(\"TOOL: analyze_impact\")\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"\n",
|
||||
"impact_json = decision_kit.analyze_impact(decision_id=decision_id)\n",
|
||||
"impact = json.loads(impact_json)\n",
|
||||
"print(\"Impact analysis:\")\n",
|
||||
"for k, v in impact.items():\n",
|
||||
" if k != \"decision_id\":\n",
|
||||
" print(f\" {k}: {v}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-summary",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── 5e. Decision Summary ─────────────────────────────────────────────────────\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"print(\"TOOL: get_decision_summary\")\n",
|
||||
"print(\"=\" * 60)\n",
|
||||
"\n",
|
||||
"summary_json = decision_kit.get_decision_summary(category=\"loan_approval\")\n",
|
||||
"summary = json.loads(summary_json)\n",
|
||||
"print(\"Decision history summary:\")\n",
|
||||
"for k, v in summary.items():\n",
|
||||
" if k not in (\"category_filter\",):\n",
|
||||
" print(f\" {k}: {v}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "agno-run-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Run the Full Agno Agent (requires API key)\n",
|
||||
"\n",
|
||||
"When `AGNO_AVAILABLE=True` and an OpenAI key is set, the LLM orchestrates all the tool calls automatically."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "run-agent",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"NEW_CASE = (\n",
|
||||
" \"New mortgage application received:\\n\"\n",
|
||||
" \" Credit score: 715, Annual income: $82,000\\n\"\n",
|
||||
" \" Debt-to-income: 30%, Down payment: 18%\\n\"\n",
|
||||
" \" Loan amount: $320,000 for a primary residence in Austin TX\\n\"\n",
|
||||
" \"Should we approve this application?\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"if AGNO_AVAILABLE:\n",
|
||||
" agent.print_response(NEW_CASE)\n",
|
||||
"else:\n",
|
||||
" print(\"[Agno not installed — skipping live agent run]\")\n",
|
||||
" print()\n",
|
||||
" print(\"Expected agent reasoning flow:\")\n",
|
||||
" print(\" 1. find_precedents('credit score 715, DTI 30%, down payment 18%')\")\n",
|
||||
" print(\" → 2 approved, 1 escalated among similar cases\")\n",
|
||||
" print(\" 2. check_policy(credit_score=715, dti=30, down_payment_pct=18)\")\n",
|
||||
" print(\" → compliant=True, violations=[]\")\n",
|
||||
" print(\" 3. record_decision(outcome='approved', confidence=0.91)\")\n",
|
||||
" print(\" → decision_id recorded in Semantica KG\")\n",
|
||||
" print()\n",
|
||||
" print(\" Recommendation: APPROVE — 3 precedents + full policy compliance\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "analytics-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. Post-Session Analytics with Semantica\n",
|
||||
"\n",
|
||||
"After the agent session, use **native Semantica APIs** for reporting and causal analysis — no Agno required."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "analytics",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Query decision history directly from Semantica\n",
|
||||
"insights = store.context.get_context_insights()\n",
|
||||
"print(\"Session Insights (Semantica native):\")\n",
|
||||
"if isinstance(insights, dict):\n",
|
||||
" for k, v in insights.items():\n",
|
||||
" print(f\" {k}: {v}\")\n",
|
||||
"else:\n",
|
||||
" print(f\" {insights}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "precedents-direct",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Precedent search directly via Semantica's AgentContext\n",
|
||||
"# (same data, no Agno in the loop)\n",
|
||||
"precedents = store.context.find_precedents_advanced(\n",
|
||||
" scenario=\"borderline mortgage application\",\n",
|
||||
" category=\"loan_approval\",\n",
|
||||
")\n",
|
||||
"print(f\"\\nPrecedent search via Semantica directly → {len(precedents or [])} results\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "summary-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summary\n",
|
||||
"\n",
|
||||
"| What | How |\n",
|
||||
"|---|---|\n",
|
||||
"| Persistent decision history | `AgnoContextStore` wrapping `AgentContext` + FAISS |\n",
|
||||
"| Tool calls for decision intelligence | `AgnoDecisionKit` (record, find, trace, check, summarise) |\n",
|
||||
"| Historical seeding | Native `AgentContext.record_decision()` — no Agno needed |\n",
|
||||
"| Policy rules | Native `PolicyEngine` — no Agno needed |\n",
|
||||
"| Post-session analytics | Native `AgentContext.get_context_insights()` — no Agno needed |\n",
|
||||
"\n",
|
||||
"The Agno integration is a **thin wrapper** — Semantica's full API remains directly accessible whenever you need finer control."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "title",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Agno × Semantica: GraphRAG Context Agent\n",
|
||||
"\n",
|
||||
"This notebook demonstrates how to give an Agno agent a **relational knowledge graph** instead of a flat document store. The agent retrieves answers via **multi-hop graph traversal** — finding connections that pure vector search misses.\n",
|
||||
"\n",
|
||||
"**Domain:** Regulatory compliance (Basel IV / DORA) — documents are ingested, entities & relations extracted, then the agent answers questions by hopping through the graph.\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Architecture\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"Agno Agent\n",
|
||||
" ├── knowledge=AgnoKnowledgeGraph ← GraphRAG knowledge base\n",
|
||||
" └── tools=[AgnoKGToolkit] ← live graph building/query tools\n",
|
||||
" │\n",
|
||||
" │ Backed by Semantica:\n",
|
||||
" ├── NERExtractor ← named entity recognition\n",
|
||||
" ├── RelationExtractor ← relation extraction\n",
|
||||
" ├── GraphBuilder ← builds ContextGraph from extractions\n",
|
||||
" ├── ContextGraph ← in-memory graph with analytics\n",
|
||||
" └── Reasoner ← rule-based inference\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"## Install\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"pip install semantica[agno]\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "imports-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Imports — Semantica Core + Agno Integration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "imports",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys, os, json\n",
|
||||
"sys.path.insert(0, os.path.abspath(\"../../\"))\n",
|
||||
"\n",
|
||||
"# ── Semantica core — used directly for pipeline setup ───────────────────────\n",
|
||||
"from semantica.kg import GraphBuilder\n",
|
||||
"from semantica.context import ContextGraph\n",
|
||||
"from semantica.semantic_extract import NERExtractor, RelationExtractor, TripletExtractor\n",
|
||||
"from semantica.reasoning import Reasoner\n",
|
||||
"from semantica.vector_store import VectorStore\n",
|
||||
"\n",
|
||||
"# ── Agno integration layer ───────────────────────────────────────────────────\n",
|
||||
"from integrations.agno import AgnoKnowledgeGraph, AgnoKGToolkit, AGNO_AVAILABLE\n",
|
||||
"\n",
|
||||
"print(\"Semantica imports OK\")\n",
|
||||
"print(f\"Agno installed: {AGNO_AVAILABLE}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "pipeline-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Build the Semantica Extraction Pipeline\n",
|
||||
"\n",
|
||||
"The extraction pipeline (NER → relation extraction → graph build) is pure Semantica. We construct each component explicitly so we can also use them for analysis outside Agno."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "build-pipeline",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# NER — identifies organisations, regulations, dates, amounts, roles\n",
|
||||
"ner = NERExtractor()\n",
|
||||
"\n",
|
||||
"# Relation extractor — finds typed edges between entities\n",
|
||||
"rel_extractor = RelationExtractor(confidence_threshold=0.60)\n",
|
||||
"\n",
|
||||
"# Knowledge graph builder\n",
|
||||
"graph_builder = GraphBuilder(merge_entities=True, temporal_support=True)\n",
|
||||
"\n",
|
||||
"# In-memory context graph (swap to neo4j/falkordb for persistence)\n",
|
||||
"context_graph = ContextGraph(advanced_analytics=True)\n",
|
||||
"\n",
|
||||
"# Reasoner for rule inference over the graph\n",
|
||||
"reasoner = Reasoner()\n",
|
||||
"\n",
|
||||
"print(\"Semantica extraction pipeline assembled\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ingest-raw-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Direct Semantica Extraction (Before Agno)\n",
|
||||
"\n",
|
||||
"We first demonstrate extraction using **raw Semantica APIs** so you can see exactly what goes into the graph.\n",
|
||||
"This is the same pipeline `AgnoKnowledgeGraph.load()` runs internally."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "raw-documents",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Regulatory documents (representative snippets)\n",
|
||||
"REGULATORY_DOCS = [\n",
|
||||
" {\n",
|
||||
" \"title\": \"Basel IV — Capital Requirements\",\n",
|
||||
" \"text\": (\n",
|
||||
" \"Basel IV introduces a revised standardised approach for credit risk, \"\n",
|
||||
" \"replacing internal model floors. Banks must maintain a minimum CET1 ratio \"\n",
|
||||
" \"of 4.5% and a total capital ratio of 8%. The BCBS finalised these requirements \"\n",
|
||||
" \"in December 2017 with a phased implementation starting January 2022. \"\n",
|
||||
" \"National regulators including the EBA and FCA are responsible for local \"\n",
|
||||
" \"transposition. Risk-weighted assets under Basel IV are calculated using \"\n",
|
||||
" \"the Output Floor, capping RWA reductions at 72.5%.\"\n",
|
||||
" ),\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"title\": \"DORA — Digital Operational Resilience Act\",\n",
|
||||
" \"text\": (\n",
|
||||
" \"DORA (Regulation EU 2022/2554) applies to financial entities and ICT \"\n",
|
||||
" \"third-party service providers operating in the EU. It mandates ICT risk \"\n",
|
||||
" \"management frameworks, incident classification, and annual operational \"\n",
|
||||
" \"resilience testing. Supervised entities must report major ICT incidents to \"\n",
|
||||
" \"the European Supervisory Authorities (ESAs) within 4 hours of classification. \"\n",
|
||||
" \"Critical ICT providers are subject to direct oversight by the Joint Oversight \"\n",
|
||||
" \"Network led by ESMA, EBA, and EIOPA. DORA became applicable on 17 January 2025.\"\n",
|
||||
" ),\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"title\": \"AML — Anti-Money Laundering Directive VI\",\n",
|
||||
" \"text\": (\n",
|
||||
" \"AMLD6 strengthens the EU's anti-money laundering framework by extending \"\n",
|
||||
" \"criminal liability to 22 predicate offences including cybercrime and \"\n",
|
||||
" \"environmental crime. Financial institutions must apply Customer Due Diligence \"\n",
|
||||
" \"(CDD) at onboarding and Enhanced Due Diligence (EDD) for high-risk customers. \"\n",
|
||||
" \"Suspicious Activity Reports (SARs) are filed with the national Financial \"\n",
|
||||
" \"Intelligence Unit (FIU). Non-compliance carries penalties up to 10% of \"\n",
|
||||
" \"annual global turnover. AMLD6 was transposed into UK law via MLCO 2020.\"\n",
|
||||
" ),\n",
|
||||
" },\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"print(f\"Documents to ingest: {len(REGULATORY_DOCS)}\")\n",
|
||||
"for doc in REGULATORY_DOCS:\n",
|
||||
" print(f\" • {doc['title']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "run-ner",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Run NER directly with Semantica ─────────────────────────────────────────\n",
|
||||
"all_entities = []\n",
|
||||
"for doc in REGULATORY_DOCS:\n",
|
||||
" entities = ner.extract_entities(doc['text']) or []\n",
|
||||
" all_entities.extend(entities)\n",
|
||||
" print(f\"[{doc['title']}] → {len(entities)} entities\")\n",
|
||||
" for e in entities[:4]:\n",
|
||||
" print(f\" {getattr(e,'name','?'):30s} type={getattr(e,'type','?')} conf={getattr(e,'confidence',0):.2f}\")\n",
|
||||
"\n",
|
||||
"print(f\"\\nTotal entities extracted: {len(all_entities)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "run-rel",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Run relation extraction directly with Semantica ──────────────────────────\n",
|
||||
"all_relations = []\n",
|
||||
"for doc in REGULATORY_DOCS:\n",
|
||||
" relations = rel_extractor.extract_relations(doc['text']) or []\n",
|
||||
" all_relations.extend(relations)\n",
|
||||
" print(f\"[{doc['title']}] → {len(relations)} relations\")\n",
|
||||
" for r in relations[:3]:\n",
|
||||
" src = getattr(r, 'source', '?')\n",
|
||||
" rtype = getattr(r, 'type', getattr(r, 'relation', '?'))\n",
|
||||
" tgt = getattr(r, 'target', '?')\n",
|
||||
" conf = getattr(r, 'confidence', 0)\n",
|
||||
" print(f\" {src!s:20s} --[{rtype}]--> {tgt!s:20s} conf={conf:.2f}\")\n",
|
||||
"\n",
|
||||
"print(f\"\\nTotal relations extracted: {len(all_relations)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "agno-kg-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Build AgnoKnowledgeGraph\n",
|
||||
"\n",
|
||||
"`AgnoKnowledgeGraph` wraps the extraction pipeline and implements Agno's `AgentKnowledge` protocol. It runs the same NER + relation extract + graph build pipeline internally — here we pass our pre-built components so the same instances are used."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "build-agno-kg",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"kg = AgnoKnowledgeGraph(\n",
|
||||
" graph_builder=graph_builder,\n",
|
||||
" ner_extractor=ner,\n",
|
||||
" relation_extractor=rel_extractor,\n",
|
||||
" context_graph=context_graph,\n",
|
||||
" num_documents=5,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Ingest all documents through the integration wrapper\n",
|
||||
"kg.load(texts=[doc['text'] for doc in REGULATORY_DOCS])\n",
|
||||
"\n",
|
||||
"print(f\"AgnoKnowledgeGraph: {len(kg._docs)} documents indexed\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "graphrag-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. GraphRAG Search\n",
|
||||
"\n",
|
||||
"The `search()` method implements **multi-hop GraphRAG**:\n",
|
||||
"1. Vector similarity over stored document texts\n",
|
||||
"2. Entity lookup in the context graph\n",
|
||||
"3. Graph hop expansion for entity neighbourhood\n",
|
||||
"4. Context injection into the returned documents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "graphrag-search",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"queries = [\n",
|
||||
" \"What is the minimum CET1 ratio required under Basel IV?\",\n",
|
||||
" \"Which authorities supervise critical ICT providers under DORA?\",\n",
|
||||
" \"What are the reporting timelines for major ICT incidents?\",\n",
|
||||
" \"How does AMLD6 handle customer due diligence?\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for query in queries:\n",
|
||||
" print(f\"\\nQ: {query}\")\n",
|
||||
" results = kg.search(query, num_documents=2)\n",
|
||||
" print(f\" Retrieved {len(results)} document(s)\")\n",
|
||||
" for i, doc in enumerate(results, 1):\n",
|
||||
" content = getattr(doc, 'content', str(doc))\n",
|
||||
" print(f\" [{i}] {content[:120]}...\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "entity-context",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Get graph context for a specific entity\n",
|
||||
"entity_contexts = [\"BCBS\", \"EBA\", \"DORA\", \"Basel IV\"]\n",
|
||||
"for entity in entity_contexts:\n",
|
||||
" ctx = kg.get_graph_context(entity)\n",
|
||||
" print(f\"\\nGraph context for '{entity}':\")\n",
|
||||
" print(ctx if ctx else \" (no graph nodes found — depends on NER extraction quality)\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "toolkit-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. AgnoKGToolkit — Live Graph Building\n",
|
||||
"\n",
|
||||
"The `AgnoKGToolkit` exposes 7 tools the LLM can call to **actively modify and query the graph** during reasoning."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "build-toolkit",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"toolkit = AgnoKGToolkit(\n",
|
||||
" ner_extractor=ner,\n",
|
||||
" relation_extractor=rel_extractor,\n",
|
||||
" reasoner=reasoner,\n",
|
||||
" context=context_graph, # share same graph as knowledge base\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"AgnoKGToolkit: {len(toolkit._tools)} tools\")\n",
|
||||
"print(\" Tools:\", [fn.__name__ for fn in toolkit._tools])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-extract-entities",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# TOOL: extract_entities\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"print(\"TOOL: extract_entities\")\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"\n",
|
||||
"new_text = (\n",
|
||||
" \"The PRA published a consultation paper requiring UK banks to \"\n",
|
||||
" \"implement DORA-equivalent resilience testing by Q3 2025, \"\n",
|
||||
" \"with Barclays and HSBC named as systemic institutions.\"\n",
|
||||
")\n",
|
||||
"entities_json = toolkit.extract_entities(new_text)\n",
|
||||
"entities_result = json.loads(entities_json)\n",
|
||||
"print(f\"Found {entities_result['count']} entities:\")\n",
|
||||
"for e in entities_result['entities']:\n",
|
||||
" print(f\" {e['name']:30s} type={e['type']:15s} conf={e['confidence']:.2f}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-extract-relations",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# TOOL: extract_relations\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"print(\"TOOL: extract_relations\")\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"\n",
|
||||
"relations_json = toolkit.extract_relations(new_text)\n",
|
||||
"relations_result = json.loads(relations_json)\n",
|
||||
"print(f\"Found {relations_result['count']} relations:\")\n",
|
||||
"for r in relations_result['relations']:\n",
|
||||
" print(f\" {r['source']:20s} --[{r['relation']}]--> {r['target']:20s}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-add-graph",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# TOOL: add_to_graph\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"print(\"TOOL: add_to_graph\")\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"\n",
|
||||
"add_result = json.loads(toolkit.add_to_graph(\n",
|
||||
" entities=json.dumps([\n",
|
||||
" {\"name\": \"PRA\", \"type\": \"REGULATOR\"},\n",
|
||||
" {\"name\": \"Barclays\", \"type\": \"BANK\"},\n",
|
||||
" {\"name\": \"HSBC\", \"type\": \"BANK\"},\n",
|
||||
" ]),\n",
|
||||
" relations=json.dumps([\n",
|
||||
" {\"source\": \"PRA\", \"relation\": \"SUPERVISES\", \"target\": \"Barclays\"},\n",
|
||||
" {\"source\": \"PRA\", \"relation\": \"SUPERVISES\", \"target\": \"HSBC\"},\n",
|
||||
" {\"source\": \"Barclays\", \"relation\": \"SUBJECT_TO\", \"target\": \"DORA\"},\n",
|
||||
" {\"source\": \"HSBC\", \"relation\": \"SUBJECT_TO\", \"target\": \"DORA\"},\n",
|
||||
" ]),\n",
|
||||
"))\n",
|
||||
"print(f\"Added: {add_result['nodes_added']} nodes, {add_result['edges_added']} edges\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-query-graph",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# TOOL: query_graph\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"print(\"TOOL: query_graph\")\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"\n",
|
||||
"query_result = json.loads(toolkit.query_graph(\"PRA\"))\n",
|
||||
"print(f\"Keyword query 'PRA' → {query_result['count']} node(s):\")\n",
|
||||
"for node in query_result['results']:\n",
|
||||
" print(f\" label={node.get('label')} type={node.get('type')}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-find-related",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# TOOL: find_related\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"print(\"TOOL: find_related\")\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"\n",
|
||||
"related_result = json.loads(toolkit.find_related(\"Barclays\", hops=2))\n",
|
||||
"print(f\"Related to 'Barclays' (2 hops): {related_result['count']} entity/entities\")\n",
|
||||
"for name in related_result['related']:\n",
|
||||
" print(f\" → {name}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-infer",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# TOOL: infer_facts — Semantica's Reasoner derives new facts from graph state\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"print(\"TOOL: infer_facts\")\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"\n",
|
||||
"# Rules: regulatory compliance inference\n",
|
||||
"inference_rules = json.dumps([\n",
|
||||
" \"IF BANK(?x) THEN FinancialEntity(?x)\",\n",
|
||||
" \"IF REGULATOR(?x) THEN SupervisoryAuthority(?x)\",\n",
|
||||
" \"IF FinancialEntity(?x) THEN ComplianceSubject(?x)\",\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"infer_result = json.loads(toolkit.infer_facts(rules=inference_rules))\n",
|
||||
"print(f\"Inferred {infer_result['count']} new fact(s):\")\n",
|
||||
"for fact in infer_result['inferred_facts'][:8]:\n",
|
||||
" print(f\" {fact}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "demo-export",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# TOOL: export_subgraph — export knowledge for downstream systems\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"print(\"TOOL: export_subgraph (JSON-LD)\")\n",
|
||||
"print(\"=\" * 55)\n",
|
||||
"\n",
|
||||
"export_result = json.loads(toolkit.export_subgraph(entity=\"DORA\", format=\"json-ld\"))\n",
|
||||
"print(f\"Exported as format='{export_result['format']}'\")\n",
|
||||
"if 'data' in export_result:\n",
|
||||
" preview = str(export_result['data'])[:300]\n",
|
||||
" print(f\"Preview: {preview}...\")\n",
|
||||
"elif 'nodes' in export_result:\n",
|
||||
" print(f\"Graph nodes exported: {len(export_result['nodes'])}\")\n",
|
||||
" for node in export_result['nodes'][:5]:\n",
|
||||
" print(f\" {node}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "agno-run-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. Run the Full Agno GraphRAG Agent (requires API key)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "agno-agent",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if AGNO_AVAILABLE:\n",
|
||||
" from agno.agent import Agent\n",
|
||||
" from agno.models.openai import OpenAIChat\n",
|
||||
"\n",
|
||||
" compliance_agent = Agent(\n",
|
||||
" name=\"ComplianceAnalyst\",\n",
|
||||
" model=OpenAIChat(id=\"gpt-4o\"),\n",
|
||||
" knowledge=kg,\n",
|
||||
" search_knowledge=True,\n",
|
||||
" tools=[toolkit],\n",
|
||||
" show_tool_calls=True,\n",
|
||||
" description=(\n",
|
||||
" \"You are a regulatory compliance analyst. Use the knowledge graph \"\n",
|
||||
" \"to answer questions about Basel IV, DORA, and AML regulations. \"\n",
|
||||
" \"When answering, use find_related and query_graph to discover \"\n",
|
||||
" \"connections between regulators, rules, and institutions.\"\n",
|
||||
" ),\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" compliance_agent.print_response(\n",
|
||||
" \"Which supervisory authorities are responsible for overseeing DORA compliance \"\n",
|
||||
" \"for UK banks, and how does this relate to Basel IV capital requirements?\"\n",
|
||||
" )\n",
|
||||
"else:\n",
|
||||
" print(\"[Agno not installed — skipping live agent run]\")\n",
|
||||
" print()\n",
|
||||
" print(\"Expected reasoning flow:\")\n",
|
||||
" print(\" search_knowledge('DORA supervisory authorities UK banks')\")\n",
|
||||
" print(\" → retrieves DORA doc with graph expansion\")\n",
|
||||
" print(\" query_graph('PRA') → finds PRA node\")\n",
|
||||
" print(\" find_related('PRA', hops=2) → PRA → SUPERVISES → Barclays, HSBC\")\n",
|
||||
" print(\" find_related('Basel IV', hops=1) → capital ratio requirements\")\n",
|
||||
" print(\" Answer: PRA supervises UK banks under DORA; Basel IV CET1 requirement is 4.5%\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "semantica-analysis",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. Post-Session Graph Analysis with Semantica\n",
|
||||
"\n",
|
||||
"After the agent session, use Semantica's graph analytics directly to explore the accumulated knowledge."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "graph-analytics",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Use Semantica's GraphAnalyzer directly on the same ContextGraph\n",
|
||||
"from semantica.kg import GraphAnalyzer, CentralityCalculator, PathFinder\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" analyzer = GraphAnalyzer()\n",
|
||||
" analysis = analyzer.analyze_graph(context_graph)\n",
|
||||
" print(\"Graph analysis (Semantica native):\")\n",
|
||||
" if isinstance(analysis, dict):\n",
|
||||
" for k, v in list(analysis.items())[:8]:\n",
|
||||
" print(f\" {k}: {v}\")\n",
|
||||
" else:\n",
|
||||
" print(f\" {analysis}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"GraphAnalyzer: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "centrality",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Centrality — which entities are most connected / influential?\n",
|
||||
"try:\n",
|
||||
" centrality = CentralityCalculator()\n",
|
||||
" scores = centrality.calculate_degree_centrality(context_graph)\n",
|
||||
" print(\"Degree centrality (most connected entities):\")\n",
|
||||
" if isinstance(scores, dict):\n",
|
||||
" top = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:5]\n",
|
||||
" for entity, score in top:\n",
|
||||
" print(f\" {entity:30s} {score:.4f}\")\n",
|
||||
" else:\n",
|
||||
" print(f\" {scores}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"CentralityCalculator: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "summary-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summary\n",
|
||||
"\n",
|
||||
"| Component | Role | Library |\n",
|
||||
"|---|---|---|\n",
|
||||
"| `NERExtractor` | Extract regulatory entities from text | Semantica |\n",
|
||||
"| `RelationExtractor` | Extract typed edges between entities | Semantica |\n",
|
||||
"| `GraphBuilder` | Build `ContextGraph` from extractions | Semantica |\n",
|
||||
"| `Reasoner` | Infer new facts from graph state | Semantica |\n",
|
||||
"| `AgnoKnowledgeGraph` | GraphRAG `AgentKnowledge` interface | Agno integration |\n",
|
||||
"| `AgnoKGToolkit` | 7 live graph tools for the Agno LLM | Agno integration |\n",
|
||||
"| `GraphAnalyzer` / `CentralityCalculator` | Post-session analytics | Semantica |\n",
|
||||
"\n",
|
||||
"The Agno integration wraps Semantica components — the full Semantica API is available for pre/post-processing and analytics independently of the agent."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "title",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Agno × Semantica: Multi-Agent Shared Context\n",
|
||||
"\n",
|
||||
"This notebook shows how an Agno **Team** of specialist agents can share a single `ContextGraph` so they:\n",
|
||||
"\n",
|
||||
"- Never make contradictory decisions\n",
|
||||
"- Reuse each other's extracted knowledge without coupling implementations\n",
|
||||
"- Maintain a full causal audit trail across all agents\n",
|
||||
"\n",
|
||||
"**Scenario:** A product strategy team with three specialist agents:\n",
|
||||
"\n",
|
||||
"| Agent | Role | Tools |\n",
|
||||
"|---|---|---|\n",
|
||||
"| `Researcher` | Extracts competitive intelligence from text | `AgnoKGToolkit` |\n",
|
||||
"| `Analyst` | Evaluates opportunities and records decisions | `AgnoDecisionKit` |\n",
|
||||
"| `Strategist` | Synthesises both into a recommendation | both |\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Architecture\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"AgnoSharedContext (single ContextGraph + VectorStore)\n",
|
||||
" │\n",
|
||||
" ├── bind_agent(\"researcher\") → AgnoContextStore (role-scoped)\n",
|
||||
" ├── bind_agent(\"analyst\") → AgnoContextStore (role-scoped)\n",
|
||||
" └── bind_agent(\"strategist\") → AgnoContextStore (role-scoped)\n",
|
||||
"\n",
|
||||
"Agno Team\n",
|
||||
" ├── Researcher memory=researcher_store tools=[AgnoKGToolkit(context=shared)]\n",
|
||||
" ├── Analyst memory=analyst_store tools=[AgnoDecisionKit(context=shared)]\n",
|
||||
" └── Strategist memory=strategist_store tools=[AgnoKGToolkit, AgnoDecisionKit]\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"## Install\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"pip install semantica[agno]\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "imports-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "imports",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys, os, json\n",
|
||||
"sys.path.insert(0, os.path.abspath(\"../../\"))\n",
|
||||
"\n",
|
||||
"# ── Semantica core ───────────────────────────────────────────────────────────\n",
|
||||
"from semantica.context import ContextGraph, AgentContext, CausalChainAnalyzer\n",
|
||||
"from semantica.vector_store import VectorStore\n",
|
||||
"from semantica.semantic_extract import NERExtractor, RelationExtractor\n",
|
||||
"from semantica.reasoning import Reasoner\n",
|
||||
"from semantica.kg import GraphBuilder, GraphAnalyzer, CentralityCalculator\n",
|
||||
"\n",
|
||||
"# ── Agno integration ─────────────────────────────────────────────────────────\n",
|
||||
"from integrations.agno import (\n",
|
||||
" AgnoSharedContext,\n",
|
||||
" AgnoDecisionKit,\n",
|
||||
" AgnoKGToolkit,\n",
|
||||
" AGNO_AVAILABLE,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"Semantica imports OK\")\n",
|
||||
"print(f\"Agno installed: {AGNO_AVAILABLE}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "shared-context-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Build the Shared Semantica Backend\n",
|
||||
"\n",
|
||||
"A single `VectorStore` and `ContextGraph` underpin the entire team. All agents read and write to the same store — role scoping is applied automatically by `AgnoSharedContext`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "build-shared",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ── Single shared backends ───────────────────────────────────────────────────\n",
|
||||
"shared_vector_store = VectorStore(backend=\"faiss\", dimension=768)\n",
|
||||
"shared_graph = ContextGraph(advanced_analytics=True)\n",
|
||||
"\n",
|
||||
"print(\"Shared VectorStore (FAISS) ready\")\n",
|
||||
"print(\"Shared ContextGraph ready\")\n",
|
||||
"\n",
|
||||
"# ── AgnoSharedContext: the team coordinator ───────────────────────────────────\n",
|
||||
"shared = AgnoSharedContext(\n",
|
||||
" vector_store=shared_vector_store,\n",
|
||||
" knowledge_graph=shared_graph,\n",
|
||||
" decision_tracking=True,\n",
|
||||
" session_id=\"product_strategy_team_q1_2026\",\n",
|
||||
")\n",
|
||||
"print(f\"\\nAgnoSharedContext ready — session: {shared.session_id}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bind-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Bind Agent Roles\n",
|
||||
"\n",
|
||||
"Each agent gets a **role-scoped** `AgnoContextStore` via `bind_agent()`. All agents share the same underlying graph, but their writes are tagged with their role for filtering."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "bind-agents",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Bind each agent role — idempotent, can be called multiple times safely\n",
|
||||
"researcher_store = shared.bind_agent(\"researcher\")\n",
|
||||
"analyst_store = shared.bind_agent(\"analyst\")\n",
|
||||
"strategist_store = shared.bind_agent(\"strategist\")\n",
|
||||
"\n",
|
||||
"print(\"Agent roles bound:\")\n",
|
||||
"for role in shared.bound_roles:\n",
|
||||
" store = shared.bind_agent(role)\n",
|
||||
" print(f\" {role:15s} → session={store.session_id}\")\n",
|
||||
"\n",
|
||||
"# Verify all roles see the same underlying knowledge_graph\n",
|
||||
"assert researcher_store._ctx is analyst_store._ctx\n",
|
||||
"print(\"\\nAll agents share the same AgentContext ✓\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "seed-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Pre-Load Competitive Intelligence\n",
|
||||
"\n",
|
||||
"Using **native Semantica APIs**, we load a competitive landscape into the shared graph. This represents knowledge the team has accumulated from prior research sessions."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "seed-intel",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Competitive intelligence documents\n",
|
||||
"COMPETITIVE_INTEL = [\n",
|
||||
" {\n",
|
||||
" \"source\": \"market_research_q4_2025\",\n",
|
||||
" \"text\": (\n",
|
||||
" \"Competitor Alpha launched a new SaaS analytics platform in Q4 2025. \"\n",
|
||||
" \"The product targets mid-market enterprises with annual revenue between \"\n",
|
||||
" \"$50M–$500M and has attracted 200 paying customers within 3 months. \"\n",
|
||||
" \"Pricing is $2,000/seat/year with volume discounts at 50+ seats. \"\n",
|
||||
" \"Alpha raised a $80M Series C led by Sequoia Capital in November 2025.\"\n",
|
||||
" ),\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"source\": \"customer_interviews_q4_2025\",\n",
|
||||
" \"text\": (\n",
|
||||
" \"Customer interviews reveal strong demand for AI-powered anomaly detection \"\n",
|
||||
" \"in financial reporting workflows. 78% of CFOs surveyed cite 'time to insight' \"\n",
|
||||
" \"as the top pain point — currently averaging 14 days per reporting cycle. \"\n",
|
||||
" \"Competitor Alpha scores poorly on integration depth (NPS: 24) while \"\n",
|
||||
" \"our legacy product scores 41. Customers value our data governance features \"\n",
|
||||
" \"but want a modern UI and sub-second query times.\"\n",
|
||||
" ),\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"source\": \"technology_scan_q4_2025\",\n",
|
||||
" \"text\": (\n",
|
||||
" \"Emerging technologies for consideration: LLM-native analytics interfaces \"\n",
|
||||
" \"reduce time-to-insight by 60% in pilot studies (Stanford HAI, 2025). \"\n",
|
||||
" \"Graph-based anomaly detection outperforms time-series approaches for \"\n",
|
||||
" \"multi-entity financial fraud by 34% (ACM SIGMOD 2025). \"\n",
|
||||
" \"Vector database adoption in enterprise analytics grew 120% YoY. \"\n",
|
||||
" \"Apache Arrow and DuckDB emerging as standards for in-process OLAP.\"\n",
|
||||
" ),\n",
|
||||
" },\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# Use Semantica NER + RelationExtractor directly for rich extraction\n",
|
||||
"ner = NERExtractor()\n",
|
||||
"rel_extractor = RelationExtractor(confidence_threshold=0.55)\n",
|
||||
"graph_builder = GraphBuilder(merge_entities=True)\n",
|
||||
"\n",
|
||||
"for doc in COMPETITIVE_INTEL:\n",
|
||||
" text = doc['text']\n",
|
||||
" entities = ner.extract_entities(text) or []\n",
|
||||
" relations = rel_extractor.extract_relations(text) or []\n",
|
||||
" print(f\"[{doc['source']}]\")\n",
|
||||
" print(f\" Entities: {len(entities)}, Relations: {len(relations)}\")\n",
|
||||
" # Store into shared context for all agents to access\n",
|
||||
" shared._context.store(text, conversation_id=doc['source'])\n",
|
||||
"\n",
|
||||
"print(\"\\nCompetitive intelligence loaded into shared context\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "tools-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Build Agent-Specific Tools\n",
|
||||
"\n",
|
||||
"Each toolkit is pointed at the **shared context** so tool calls across agents modify and read the same graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "build-tools",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Researcher's KG toolkit — builds knowledge from raw text\n",
|
||||
"researcher_kg_kit = AgnoKGToolkit(\n",
|
||||
" ner_extractor=ner,\n",
|
||||
" relation_extractor=rel_extractor,\n",
|
||||
" reasoner=Reasoner(),\n",
|
||||
" context=shared.knowledge_graph, # shared graph\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Analyst's decision kit — records evaluations and finds precedents\n",
|
||||
"analyst_decision_kit = AgnoDecisionKit(\n",
|
||||
" context=shared._context, # shared AgentContext\n",
|
||||
" max_precedents=5,\n",
|
||||
" causal_depth=3,\n",
|
||||
" enable_policy_check=True,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Strategist gets both\n",
|
||||
"strategist_kg_kit = AgnoKGToolkit(\n",
|
||||
" ner_extractor=ner,\n",
|
||||
" relation_extractor=rel_extractor,\n",
|
||||
" reasoner=Reasoner(),\n",
|
||||
" context=shared.knowledge_graph,\n",
|
||||
")\n",
|
||||
"strategist_decision_kit = AgnoDecisionKit(\n",
|
||||
" context=shared._context,\n",
|
||||
" max_precedents=5,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Researcher toolkit: {len(researcher_kg_kit._tools)} tools\")\n",
|
||||
"print(f\"Analyst toolkit: {len(analyst_decision_kit._tools)} tools\")\n",
|
||||
"print(f\"Strategist toolkits: {len(strategist_kg_kit._tools)} + {len(strategist_decision_kit._tools)} tools\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "simulate-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Simulate Agent Collaboration\n",
|
||||
"\n",
|
||||
"We simulate the agents' reasoning steps directly, showing how shared context propagates knowledge between roles."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "researcher-turn",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(\"=\" * 65)\n",
|
||||
"print(\"RESEARCHER AGENT TURN\")\n",
|
||||
"print(\"=\" * 65)\n",
|
||||
"\n",
|
||||
"# Researcher extracts entities from new competitive intel\n",
|
||||
"new_intel = (\n",
|
||||
" \"Competitor Beta just closed a strategic partnership with Microsoft Azure, \"\n",
|
||||
" \"integrating their anomaly detection engine natively into Azure Synapse Analytics. \"\n",
|
||||
" \"This gives Beta access to Microsoft's 300,000+ enterprise customer base. \"\n",
|
||||
" \"Beta's CEO Sarah Chen announced the deal at Gartner Data & Analytics Summit.\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Step 1: Extract entities\n",
|
||||
"entities_result = json.loads(researcher_kg_kit.extract_entities(new_intel))\n",
|
||||
"print(f\"\\n[researcher] extracted {entities_result['count']} entities:\")\n",
|
||||
"for e in entities_result['entities']:\n",
|
||||
" print(f\" {e['name']:30s} type={e['type']}\")\n",
|
||||
"\n",
|
||||
"# Step 2: Extract relations\n",
|
||||
"relations_result = json.loads(researcher_kg_kit.extract_relations(new_intel))\n",
|
||||
"print(f\"\\n[researcher] extracted {relations_result['count']} relations\")\n",
|
||||
"\n",
|
||||
"# Step 3: Add to shared graph — now visible to ALL agents\n",
|
||||
"add_result = json.loads(researcher_kg_kit.add_to_graph(\n",
|
||||
" entities=json.dumps([\n",
|
||||
" {\"name\": \"Competitor Beta\", \"type\": \"COMPANY\"},\n",
|
||||
" {\"name\": \"Microsoft Azure\", \"type\": \"COMPANY\"},\n",
|
||||
" {\"name\": \"Azure Synapse Analytics\", \"type\": \"PRODUCT\"},\n",
|
||||
" {\"name\": \"Sarah Chen\", \"type\": \"PERSON\"},\n",
|
||||
" {\"name\": \"Gartner Data & Analytics Summit\", \"type\": \"EVENT\"},\n",
|
||||
" ]),\n",
|
||||
" relations=json.dumps([\n",
|
||||
" {\"source\": \"Competitor Beta\", \"relation\": \"PARTNERSHIP_WITH\", \"target\": \"Microsoft Azure\"},\n",
|
||||
" {\"source\": \"Competitor Beta\", \"relation\": \"INTEGRATES_WITH\", \"target\": \"Azure Synapse Analytics\"},\n",
|
||||
" {\"source\": \"Sarah Chen\", \"relation\": \"CEO_OF\", \"target\": \"Competitor Beta\"},\n",
|
||||
" ]),\n",
|
||||
"))\n",
|
||||
"print(f\"\\n[researcher] added {add_result['nodes_added']} nodes, {add_result['edges_added']} edges to SHARED graph\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "analyst-turn",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(\"=\" * 65)\n",
|
||||
"print(\"ANALYST AGENT TURN (sees researcher's graph additions)\")\n",
|
||||
"print(\"=\" * 65)\n",
|
||||
"\n",
|
||||
"# Analyst queries the graph the researcher just populated\n",
|
||||
"competitor_query = json.loads(analyst_decision_kit.find_precedents(\n",
|
||||
" scenario=\"competitor partnership with cloud hyperscaler threatens market position\",\n",
|
||||
" limit=3,\n",
|
||||
"))\n",
|
||||
"print(f\"\\n[analyst] find_precedents → {competitor_query['count']} similar past strategic responses found\")\n",
|
||||
"\n",
|
||||
"# Analyst records a strategic evaluation decision\n",
|
||||
"eval_json = analyst_decision_kit.record_decision(\n",
|
||||
" category=\"strategic_response\",\n",
|
||||
" scenario=(\n",
|
||||
" \"Competitor Beta + Microsoft Azure partnership gives Beta access to \"\n",
|
||||
" \"300k enterprise customers via Azure Synapse native integration\"\n",
|
||||
" ),\n",
|
||||
" reasoning=(\n",
|
||||
" \"Threat level: HIGH. Beta's Azure native integration removes our \"\n",
|
||||
" \"integration advantage. Existing NPS lead (41 vs 24) remains but \"\n",
|
||||
" \"distribution disadvantage is critical. Recommend accelerated cloud-native \"\n",
|
||||
" \"partnership evaluation, specifically AWS Marketplace + Snowflake Native App.\"\n",
|
||||
" ),\n",
|
||||
" outcome=\"escalate_to_strategy\",\n",
|
||||
" confidence=0.85,\n",
|
||||
" entities=\"Competitor Beta, Microsoft Azure, AWS Marketplace, Snowflake\",\n",
|
||||
")\n",
|
||||
"eval_result = json.loads(eval_json)\n",
|
||||
"analyst_decision_id = eval_result['decision_id']\n",
|
||||
"print(f\"\\n[analyst] recorded evaluation → decision_id: {analyst_decision_id}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "strategist-turn",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(\"=\" * 65)\n",
|
||||
"print(\"STRATEGIST AGENT TURN (sees both researcher + analyst work)\")\n",
|
||||
"print(\"=\" * 65)\n",
|
||||
"\n",
|
||||
"# Strategist queries the graph for the full competitive picture\n",
|
||||
"related = json.loads(strategist_kg_kit.find_related(\"Competitor Beta\", hops=2))\n",
|
||||
"print(f\"\\n[strategist] 'Competitor Beta' 2-hop neighbourhood: {related['count']} entity/entities\")\n",
|
||||
"for entity in related['related']:\n",
|
||||
" print(f\" → {entity}\")\n",
|
||||
"\n",
|
||||
"# Strategist traces what the analyst decided\n",
|
||||
"causal = json.loads(strategist_decision_kit.trace_causal_chain(analyst_decision_id, depth=3))\n",
|
||||
"print(f\"\\n[strategist] causal chain for analyst decision: {causal}\")\n",
|
||||
"\n",
|
||||
"# Strategist records the final strategic recommendation\n",
|
||||
"strategy_json = strategist_decision_kit.record_decision(\n",
|
||||
" category=\"product_strategy\",\n",
|
||||
" scenario=\"Q1 2026 product strategy: respond to Beta+Azure threat\",\n",
|
||||
" reasoning=(\n",
|
||||
" \"Based on researcher's KG (Beta+Azure integration, 300k customer reach) \"\n",
|
||||
" \"and analyst's evaluation (threat level HIGH, escalated decision). \"\n",
|
||||
" \"Strategy: (1) Accelerate AWS Marketplace listing by Q2 2026. \"\n",
|
||||
" \"(2) Launch Snowflake Native App by Q3 2026. \"\n",
|
||||
" \"(3) Invest $2M in UI modernisation to widen NPS lead. \"\n",
|
||||
" \"(4) Fast-track LLM-native analytics interface (60% time-to-insight improvement per HAI study). \"\n",
|
||||
" \"Existing NPS advantage (41 vs 24) provides 18-month window before Beta catches up.\"\n",
|
||||
" ),\n",
|
||||
" outcome=\"approved\",\n",
|
||||
" confidence=0.88,\n",
|
||||
" entities=\"AWS Marketplace, Snowflake, LLM Analytics, Q2 2026, Q3 2026\",\n",
|
||||
")\n",
|
||||
"strategy_result = json.loads(strategy_json)\n",
|
||||
"print(f\"\\n[strategist] final recommendation recorded → {strategy_result['decision_id']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "shared-pool-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. Verify Shared Memory Pool\n",
|
||||
"\n",
|
||||
"Memories written by one agent are readable by all others."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "verify-shared",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from integrations.agno.context_store import _MemoryRow as MemoryRow\n",
|
||||
"\n",
|
||||
"# Researcher writes a memory\n",
|
||||
"researcher_row = MemoryRow(\n",
|
||||
" memory=\"Beta + Azure partnership announced at Gartner Summit — threat level HIGH\",\n",
|
||||
" user_id=\"researcher\",\n",
|
||||
")\n",
|
||||
"researcher_store.upsert_memory(researcher_row)\n",
|
||||
"\n",
|
||||
"# Analyst writes a memory\n",
|
||||
"analyst_row = MemoryRow(\n",
|
||||
" memory=\"NPS advantage (41 vs 24) gives 18-month window — accelerate cloud partnerships\",\n",
|
||||
" user_id=\"analyst\",\n",
|
||||
")\n",
|
||||
"analyst_store.upsert_memory(analyst_row)\n",
|
||||
"\n",
|
||||
"# Strategist reads ALL memories from both agents\n",
|
||||
"strategist_memories = strategist_store.read_memories()\n",
|
||||
"\n",
|
||||
"print(f\"Strategist sees {len(strategist_memories)} shared memory item(s):\")\n",
|
||||
"for m in strategist_memories:\n",
|
||||
" uid = getattr(m, 'user_id', '?')\n",
|
||||
" text = getattr(m, 'memory', str(m))\n",
|
||||
" print(f\" [{uid:12s}] {text[:80]}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "agno-team-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. Wire into Agno Team (requires API key)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "agno-team",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if AGNO_AVAILABLE:\n",
|
||||
" from agno.agent import Agent\n",
|
||||
" from agno.team import Team\n",
|
||||
" from agno.memory import AgentMemory\n",
|
||||
" from agno.models.openai import OpenAIChat\n",
|
||||
"\n",
|
||||
" researcher_agent = Agent(\n",
|
||||
" name=\"Researcher\",\n",
|
||||
" model=OpenAIChat(id=\"gpt-4o\"),\n",
|
||||
" memory=AgentMemory(db=researcher_store),\n",
|
||||
" tools=[researcher_kg_kit],\n",
|
||||
" show_tool_calls=True,\n",
|
||||
" description=(\n",
|
||||
" \"You are a competitive intelligence researcher. \"\n",
|
||||
" \"Use extract_entities, extract_relations, and add_to_graph \"\n",
|
||||
" \"to build a structured knowledge graph from market intelligence. \"\n",
|
||||
" \"Always add discoveries to the shared graph.\"\n",
|
||||
" ),\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" analyst_agent = Agent(\n",
|
||||
" name=\"Analyst\",\n",
|
||||
" model=OpenAIChat(id=\"gpt-4o\"),\n",
|
||||
" memory=AgentMemory(db=analyst_store),\n",
|
||||
" tools=[analyst_decision_kit],\n",
|
||||
" show_tool_calls=True,\n",
|
||||
" description=(\n",
|
||||
" \"You are a strategic analyst. Use find_precedents to check historical \"\n",
|
||||
" \"responses to similar threats, then record_decision with your evaluation. \"\n",
|
||||
" \"Always check if a similar situation was handled before acting.\"\n",
|
||||
" ),\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" strategist_agent = Agent(\n",
|
||||
" name=\"Strategist\",\n",
|
||||
" model=OpenAIChat(id=\"gpt-4o\"),\n",
|
||||
" memory=AgentMemory(db=strategist_store),\n",
|
||||
" tools=[strategist_kg_kit, strategist_decision_kit],\n",
|
||||
" show_tool_calls=True,\n",
|
||||
" description=(\n",
|
||||
" \"You are the Chief Strategy Officer. Synthesise the researcher's knowledge \"\n",
|
||||
" \"graph and the analyst's decision record into a concrete product strategy. \"\n",
|
||||
" \"Use find_related to explore the competitive graph, then record_decision \"\n",
|
||||
" \"with the final approved strategy.\"\n",
|
||||
" ),\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" strategy_team = Team(\n",
|
||||
" name=\"Product Strategy Team\",\n",
|
||||
" agents=[researcher_agent, analyst_agent, strategist_agent],\n",
|
||||
" mode=\"coordinate\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" strategy_team.print_response(\n",
|
||||
" \"Competitor Beta just announced a native Azure integration. \"\n",
|
||||
" \"Analyse the competitive landscape and recommend our Q1 2026 product strategy.\"\n",
|
||||
" )\n",
|
||||
"else:\n",
|
||||
" print(\"[Agno not installed — skipping live team run]\")\n",
|
||||
" print()\n",
|
||||
" print(\"Expected team coordination flow:\")\n",
|
||||
" print(\" 1. Researcher: extract_entities + add_to_graph (Beta+Azure)\")\n",
|
||||
" print(\" 2. Analyst: find_precedents + record_decision (threat=HIGH, escalate)\")\n",
|
||||
" print(\" 3. Strategist: find_related + trace_causal_chain + record_decision (final strategy)\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "post-session-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. Post-Session Analysis with Semantica\n",
|
||||
"\n",
|
||||
"After the team session, use **native Semantica APIs** for cross-agent audit, analytics, and causal chain review."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cross-agent-insights",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Team-level insights from AgnoSharedContext\n",
|
||||
"insights = shared.get_shared_insights()\n",
|
||||
"print(\"Team session insights:\")\n",
|
||||
"if isinstance(insights, dict):\n",
|
||||
" for k, v in insights.items():\n",
|
||||
" print(f\" {k}: {v}\")\n",
|
||||
"else:\n",
|
||||
" print(f\" {insights}\")\n",
|
||||
"\n",
|
||||
"print(f\"\\nBound agent roles: {shared.bound_roles}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "precedent-search",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Find all cross-agent strategic decisions\n",
|
||||
"all_strategic = shared.find_precedents(\n",
|
||||
" scenario=\"cloud partnership competitive response\",\n",
|
||||
" category=\"strategic_response\",\n",
|
||||
")\n",
|
||||
"print(f\"Cross-agent strategic precedents: {len(all_strategic or [])}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "graph-analytics",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Graph analytics on the shared knowledge graph (Semantica native)\n",
|
||||
"try:\n",
|
||||
" analyzer = GraphAnalyzer()\n",
|
||||
" analysis = analyzer.analyze_graph(shared.knowledge_graph)\n",
|
||||
" print(\"Shared knowledge graph analysis:\")\n",
|
||||
" if isinstance(analysis, dict):\n",
|
||||
" for k, v in list(analysis.items())[:6]:\n",
|
||||
" print(f\" {k}: {v}\")\n",
|
||||
" else:\n",
|
||||
" print(f\" {analysis}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"GraphAnalyzer: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "centrality-analysis",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Which entities are most central in the competitive intelligence graph?\n",
|
||||
"try:\n",
|
||||
" centrality = CentralityCalculator()\n",
|
||||
" scores = centrality.calculate_degree_centrality(shared.knowledge_graph)\n",
|
||||
" print(\"Most central entities in shared graph:\")\n",
|
||||
" if isinstance(scores, dict):\n",
|
||||
" top = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:5]\n",
|
||||
" for entity, score in top:\n",
|
||||
" print(f\" {entity:35s} centrality={score:.4f}\")\n",
|
||||
" else:\n",
|
||||
" print(f\" {scores}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"CentralityCalculator: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "causal-analysis",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Direct Semantica causal chain analysis (no Agno needed)\n",
|
||||
"try:\n",
|
||||
" causal_analyzer = CausalChainAnalyzer(graph_store=shared.knowledge_graph)\n",
|
||||
" # Query all decisions made during this session\n",
|
||||
" decisions = shared.knowledge_graph.find_precedents(category=\"product_strategy\", limit=10)\n",
|
||||
" print(f\"Product strategy decisions in shared graph: {len(decisions or [])}\")\n",
|
||||
" for d in (decisions or [])[:3]:\n",
|
||||
" scenario = d.get('scenario', '') if isinstance(d, dict) else str(d)\n",
|
||||
" outcome = d.get('outcome', '') if isinstance(d, dict) else ''\n",
|
||||
" print(f\" [{outcome:20s}] {scenario[:70]}\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"CausalChainAnalyzer: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "summary-section",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summary\n",
|
||||
"\n",
|
||||
"| Pattern | Implementation |\n",
|
||||
"|---|---|\n",
|
||||
"| Single shared knowledge graph | `AgnoSharedContext(vector_store, knowledge_graph)` |\n",
|
||||
"| Role-scoped memory | `shared.bind_agent(\"researcher\")` → `_AgentScopedStore` |\n",
|
||||
"| Cross-agent memory visibility | All stores read from `shared._shared_memories` |\n",
|
||||
"| KG tool sharing | `AgnoKGToolkit(context=shared.knowledge_graph)` |\n",
|
||||
"| Decision tool sharing | `AgnoDecisionKit(context=shared._context)` |\n",
|
||||
"| Thread-safe binding | `AgnoSharedContext._lock` (RLock) |\n",
|
||||
"| Post-session analytics | `GraphAnalyzer`, `CentralityCalculator`, `CausalChainAnalyzer` — all Semantica native |\n",
|
||||
"\n",
|
||||
"**Key design rule:** Every agent writes to the **same underlying graph** via different role-scoped stores. The Agno integration is a thin routing layer — Semantica's full power is available at any point directly."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -178,6 +178,13 @@
|
||||
"rdf_exporter.export(kg, \"output.ttl\", format=\"turtle\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": "# TTL alias: format=\"ttl\" is equivalent to format=\"turtle\"\nrdf_data = {\n \"entities\": [\n {\"id\": \"e1\", \"text\": \"Apple Inc.\", \"type\": \"ORG\", \"confidence\": 0.95},\n {\"id\": \"e2\", \"text\": \"Steve Jobs\", \"type\": \"PERSON\", \"confidence\": 0.97},\n ],\n \"relationships\": [\n {\"source_id\": \"e2\", \"target_id\": \"e1\", \"type\": \"founded_by\", \"confidence\": 0.91},\n ],\n}\n\nrdf_exporter.export(rdf_data, \"output.ttl\", format=\"ttl\")\n\nresult = rdf_exporter.validate_rdf(rdf_data)\nprint(f\"Valid: {result['overall_valid']}\")",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
|
||||
+2832
File diff suppressed because it is too large
Load Diff
+51327
File diff suppressed because it is too large
Load Diff
+86
@@ -0,0 +1,86 @@
|
||||
@prefix mcg: <https://example.org/mcg#> .
|
||||
@prefix prov: <http://www.w3.org/ns/prov#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
|
||||
<https://example.org/mcg/instance-data> a owl:Ontology ;
|
||||
rdfs:label "Military Capability Gap Analysis Instance Data" ;
|
||||
owl:imports <https://example.org/mcg> .
|
||||
|
||||
# Scenario and threat
|
||||
mcg:Scenario_FutureA2AD_2028 a mcg:Scenario ;
|
||||
rdfs:label "Future A2/AD Escalation 2028" ;
|
||||
mcg:hasThreat mcg:Threat_LowAltitudeSwarm .
|
||||
|
||||
mcg:Threat_LowAltitudeSwarm a mcg:Threat ;
|
||||
rdfs:label "Low-Altitude Swarm Threat" ;
|
||||
mcg:relatedToIntelligenceReport mcg:IntelReport_RAND_RRA733_1 .
|
||||
|
||||
# Mission thread and events
|
||||
mcg:MissionThread_ForceProtection a mcg:MissionThread ;
|
||||
rdfs:label "Force Protection under Swarm Pressure" ;
|
||||
mcg:missionPriority "high" ;
|
||||
mcg:includesEvent mcg:Event_SwarmIncursion_001 ;
|
||||
mcg:requiresCapability mcg:Capability_LowAltitudeDetection ;
|
||||
mcg:revealsGap mcg:Gap_LowAltitudeDetectionCoverage .
|
||||
|
||||
mcg:Scenario_FutureA2AD_2028 mcg:hasMissionThread mcg:MissionThread_ForceProtection .
|
||||
|
||||
mcg:Event_SwarmIncursion_001 a mcg:OperationalEvent ;
|
||||
rdfs:label "Swarm Incursion Event 001" ;
|
||||
mcg:eventTime "2028-04-12T05:15:00Z"^^xsd:dateTime ;
|
||||
mcg:stressesSystem mcg:System_GroundRadarLayer ;
|
||||
mcg:relatedToWargameObservation mcg:WargameObs_ValleyIngress .
|
||||
|
||||
# Systems and capabilities
|
||||
mcg:System_GroundRadarLayer a mcg:System ;
|
||||
rdfs:label "Ground Radar Layer" ;
|
||||
mcg:coveragePercent "42.0"^^xsd:decimal ;
|
||||
mcg:relatedToAssetRecord mcg:AssetRecord_RadarFleet_2028Q1 .
|
||||
|
||||
mcg:Capability_LowAltitudeDetection a mcg:Capability ;
|
||||
rdfs:label "Low Altitude Detection Capability" ;
|
||||
mcg:requiredCoveragePercent "75.0"^^xsd:decimal ;
|
||||
mcg:providedBy mcg:System_GroundRadarLayer .
|
||||
|
||||
# Gap and outcome
|
||||
mcg:Gap_LowAltitudeDetectionCoverage a mcg:CapabilityGap ;
|
||||
rdfs:label "Insufficient Low-Altitude Detection Coverage" ;
|
||||
mcg:gapInCapability mcg:Capability_LowAltitudeDetection ;
|
||||
mcg:gapSeverity "critical" ;
|
||||
mcg:increasesRiskOf mcg:Outcome_MissionRiskIncrease ;
|
||||
mcg:triggersDecision mcg:Decision_CapGap_001 .
|
||||
|
||||
mcg:Outcome_MissionRiskIncrease a mcg:Outcome ;
|
||||
rdfs:label "Increased Mission Risk and Response Delay" .
|
||||
|
||||
# Decision and recommendation
|
||||
mcg:Decision_CapGap_001 a mcg:Decision ;
|
||||
rdfs:label "Capability Gap Decision 001" ;
|
||||
mcg:confidenceScore "0.93"^^xsd:decimal ;
|
||||
mcg:hasRecommendation mcg:Recommendation_MultiLayerSensorFusion ;
|
||||
mcg:supportedByEvidence mcg:Evidence_E001 ;
|
||||
mcg:wasAssessedBy mcg:AnalystCell_A1 .
|
||||
|
||||
mcg:Recommendation_MultiLayerSensorFusion a mcg:Recommendation ;
|
||||
mcg:recommendationText "Integrate layered sensing (ground radar, passive RF, EO/IR) and update mission doctrine for low-altitude swarm defense." .
|
||||
|
||||
# Evidence and provenance
|
||||
mcg:Evidence_E001 a mcg:Evidence ;
|
||||
mcg:evidenceQuote "Operational analysis indicates persistent low-altitude sensing shortfalls in contested terrain." ;
|
||||
mcg:derivedFromDocument mcg:IntelReport_RAND_RRA733_1 .
|
||||
|
||||
mcg:IntelReport_RAND_RRA733_1 a mcg:IntelligenceReport, prov:Entity ;
|
||||
rdfs:label "RAND RRA733-1 Competing Without Fighting (2022)" .
|
||||
|
||||
mcg:WargameObs_ValleyIngress a mcg:WargameObservation, prov:Entity ;
|
||||
rdfs:label "Wargame Observation: Valley Ingress Routes" .
|
||||
|
||||
mcg:AssetRecord_RadarFleet_2028Q1 a mcg:AssetInventoryRecord, prov:Entity ;
|
||||
rdfs:label "Asset Inventory: Radar Fleet 2028 Q1" .
|
||||
|
||||
mcg:AnalystCell_A1 a prov:Agent ;
|
||||
rdfs:label "Joint Capability Assessment Cell A1" .
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
@prefix mcg: <https://example.org/mcg#> .
|
||||
@prefix prov: <http://www.w3.org/ns/prov#> .
|
||||
@prefix d3f: <http://d3fend.mitre.org/ontologies/d3fend.owl#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
|
||||
<https://example.org/mcg> a owl:Ontology ;
|
||||
rdfs:label "Military Capability Gap Analysis Ontology" ;
|
||||
rdfs:comment "Ontology for end-to-end military capability gap analysis with context graphs, multi-hop reasoning, and provenance." ;
|
||||
owl:imports <http://www.w3.org/ns/prov> .
|
||||
|
||||
# Classes
|
||||
mcg:Scenario a owl:Class .
|
||||
mcg:MissionThread a owl:Class .
|
||||
mcg:OperationalEvent a owl:Class .
|
||||
mcg:System a owl:Class .
|
||||
mcg:Capability a owl:Class .
|
||||
mcg:CapabilityGap a owl:Class .
|
||||
mcg:Outcome a owl:Class .
|
||||
mcg:Decision a owl:Class .
|
||||
mcg:Recommendation a owl:Class .
|
||||
mcg:Evidence a owl:Class .
|
||||
mcg:Threat a owl:Class .
|
||||
mcg:DoctrineDocument a owl:Class ;
|
||||
rdfs:subClassOf prov:Entity .
|
||||
mcg:WargameObservation a owl:Class ;
|
||||
rdfs:subClassOf prov:Entity .
|
||||
mcg:AssetInventoryRecord a owl:Class ;
|
||||
rdfs:subClassOf prov:Entity .
|
||||
mcg:IntelligenceReport a owl:Class ;
|
||||
rdfs:subClassOf prov:Entity .
|
||||
|
||||
# Optional alignment points
|
||||
mcg:Sensor a owl:Class ;
|
||||
rdfs:subClassOf mcg:System, d3f:D3FEND .
|
||||
|
||||
# Object properties (context chain)
|
||||
mcg:hasMissionThread a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:Scenario ;
|
||||
rdfs:range mcg:MissionThread .
|
||||
|
||||
mcg:includesEvent a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:MissionThread ;
|
||||
rdfs:range mcg:OperationalEvent .
|
||||
|
||||
mcg:stressesSystem a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:OperationalEvent ;
|
||||
rdfs:range mcg:System .
|
||||
|
||||
mcg:requiresCapability a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:MissionThread ;
|
||||
rdfs:range mcg:Capability .
|
||||
|
||||
mcg:providedBy a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:Capability ;
|
||||
rdfs:range mcg:System .
|
||||
|
||||
mcg:revealsGap a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:MissionThread ;
|
||||
rdfs:range mcg:CapabilityGap .
|
||||
|
||||
mcg:gapInCapability a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:CapabilityGap ;
|
||||
rdfs:range mcg:Capability .
|
||||
|
||||
mcg:increasesRiskOf a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:CapabilityGap ;
|
||||
rdfs:range mcg:Outcome .
|
||||
|
||||
mcg:triggersDecision a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:CapabilityGap ;
|
||||
rdfs:range mcg:Decision .
|
||||
|
||||
mcg:hasRecommendation a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:Decision ;
|
||||
rdfs:range mcg:Recommendation .
|
||||
|
||||
mcg:supportedByEvidence a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:Decision ;
|
||||
rdfs:range mcg:Evidence .
|
||||
|
||||
mcg:hasThreat a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:Scenario ;
|
||||
rdfs:range mcg:Threat .
|
||||
|
||||
mcg:relatedToAssetRecord a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:System ;
|
||||
rdfs:range mcg:AssetInventoryRecord .
|
||||
|
||||
mcg:relatedToWargameObservation a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:OperationalEvent ;
|
||||
rdfs:range mcg:WargameObservation .
|
||||
|
||||
mcg:relatedToIntelligenceReport a owl:ObjectProperty ;
|
||||
rdfs:domain mcg:Threat ;
|
||||
rdfs:range mcg:IntelligenceReport .
|
||||
|
||||
# Provenance properties
|
||||
mcg:derivedFromDocument a owl:ObjectProperty ;
|
||||
rdfs:subPropertyOf prov:wasDerivedFrom ;
|
||||
rdfs:domain mcg:Evidence ;
|
||||
rdfs:range prov:Entity .
|
||||
|
||||
mcg:wasAssessedBy a owl:ObjectProperty ;
|
||||
rdfs:subPropertyOf prov:wasAssociatedWith ;
|
||||
rdfs:domain mcg:Decision ;
|
||||
rdfs:range prov:Agent .
|
||||
|
||||
# Data properties
|
||||
mcg:coveragePercent a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:System ;
|
||||
rdfs:range xsd:decimal .
|
||||
|
||||
mcg:requiredCoveragePercent a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:Capability ;
|
||||
rdfs:range xsd:decimal .
|
||||
|
||||
mcg:gapSeverity a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:CapabilityGap ;
|
||||
rdfs:range xsd:string .
|
||||
|
||||
mcg:confidenceScore a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:Decision ;
|
||||
rdfs:range xsd:decimal .
|
||||
|
||||
mcg:missionPriority a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:MissionThread ;
|
||||
rdfs:range xsd:string .
|
||||
|
||||
mcg:eventTime a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:OperationalEvent ;
|
||||
rdfs:range xsd:dateTime .
|
||||
|
||||
mcg:recommendationText a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:Recommendation ;
|
||||
rdfs:range xsd:string .
|
||||
|
||||
mcg:evidenceQuote a owl:DatatypeProperty ;
|
||||
rdfs:domain mcg:Evidence ;
|
||||
rdfs:range xsd:string .
|
||||
|
||||
+2466
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
<html><head><title>Request Rejected </title></head><body>Sorry, the requested URL was rejected. Please consult with your administrator..<br><br>Your support ID is: <9627954236696643144><br><br><a href='javascript:history.back();'>[Go Back]</body></html>
|
||||
@@ -1018,7 +1018,7 @@ knowledge_graph.apply_resolutions(resolved_data)
|
||||
|
||||
### 💬 Community Support
|
||||
|
||||
- **💬 [Discord Community](https://discord.gg/N7WmAuDH)** - Real-time chat and support
|
||||
- **💬 [Discord Community](https://discord.gg/sV34vps5hH)** - Real-time chat and support
|
||||
- **🐙 [GitHub Discussions](https://github.com/semantica/semantica/discussions)** - Community Q&A
|
||||
- **📧 [Mailing List](https://groups.google.com/g/semantica)** - Announcements and updates
|
||||
- **🐦 [Twitter](https://twitter.com/semantica)** - Latest news and tips
|
||||
@@ -1051,6 +1051,6 @@ This project is licensed under the MIT License - see the [LICENSE](https://githu
|
||||
|
||||
**🚀 Ready to transform your data into intelligent knowledge?**
|
||||
|
||||
[Get Started Now](https://semantica.readthedocs.io/quickstart/) • [View Examples](https://github.com/semantica/examples) • [Join Community](https://discord.gg/N7WmAuDH)
|
||||
[Get Started Now](https://semantica.readthedocs.io/quickstart/) • [View Examples](https://github.com/semantica/examples) • [Join Community](https://discord.gg/sV34vps5hH)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
## Semantica Deduplication V2: Migration & Performance Guide
|
||||
|
||||
Welcome to the Deduplication V2 engine!! This release specifically targets severe CI delays and production bottlenecks caused by massive knowledge graph deduplication workloads. By introducing smarter candidate generation, fast-fail prefilters, and semantic triplet canonicalization, we have reduced worst-case execution times by up to **80%**.
|
||||
|
||||
**Note:** This upgrade is **100% backward compatible.** All existing scripts, tests, and API signatures will continue to work exactly as they did before.
|
||||
|
||||
|
||||
|
||||
To utilize this new addition, you must explicitly **opt-in** using the new configuration keys detailed below.
|
||||
|
||||
---
|
||||
|
||||
### 1. Candidate Generation V2 (Beating the $O(N^2)$ Pair Explosion)
|
||||
|
||||
**The Problem:** The legacy engine relied on a naive first-character blocking strategy. If your dataset contained 5,000 companies starting with letter "A", the engine generated nearly 12.5 million candidate pairs.
|
||||
|
||||
**The V2 Solution:** Multi-key token blocking, prefix matching, and deterministic candidate budgeting.
|
||||
|
||||
|
||||
|
||||
**How to Opt-In**
|
||||
|
||||
Pass the keys into the `similarity`configuration dictionary when initializing the `DuplicateDetector`:
|
||||
|
||||
|
||||
|
||||
```python
|
||||
from semantica.deduplication import DuplicateDetector
|
||||
|
||||
detector = DuplicateDetector(
|
||||
similarity_threshold=0.8,
|
||||
similarity = {
|
||||
# Switches from legacy to v2
|
||||
"candidate_strategy": "blocking_v2",
|
||||
|
||||
# Highly recommended: Limits the max number of comparisons
|
||||
# per entity to prevent adversarial latency spikes.
|
||||
"max_candidates_per_entity": 50,
|
||||
|
||||
# Optional: Generates blocks using Soundex algorithm to catch
|
||||
# phonetic misspellings (e.g, "Jon" vs "John")
|
||||
"enable_phonetic_blocking": True
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 2. Two-Stage scoring (The Fast Prefilter)
|
||||
|
||||
**The Problem**: Calculating multi-factor semantic scores (Levenshtein, Jaro-Winkler, property intersections, and Embeddings) is computationally expensive. Running these
|
||||
|
||||
calculations on two entities that share absolutely zero words or have vastly different string lengths is a waste of resources.
|
||||
|
||||
**The V2 Solution:** A lightning-fast prefilter gate that instantly drops obvious non-matches before they ever reach the heavy semantic scorers.
|
||||
|
||||
|
||||
|
||||
**How to Opt-In**
|
||||
|
||||
Enable the prefilter and define your rejection thresholds:
|
||||
|
||||
|
||||
|
||||
```python
|
||||
from semantica.deduplication import DuplicateDetector
|
||||
|
||||
detector = DuplicateDetector(
|
||||
similarity_threshold=0.8,
|
||||
similarity={
|
||||
"candidate_strategy": "blocking_v2",
|
||||
|
||||
# Enable prefilter
|
||||
"prefilter_enabled": True,
|
||||
|
||||
"prefilter_thresholds": {
|
||||
# Rejects pairs if shortest string is less than 40% the length
|
||||
# of the longest
|
||||
"min_length_ratio": 0.4,
|
||||
|
||||
# Instantly rejects pairs if they don't share at least one
|
||||
# valid word token
|
||||
"required_shared_token": True
|
||||
},
|
||||
# Optional Explainability: Injects a 'score_breakdown' dict into
|
||||
# the candidate metadata so you can see exactly how the string,
|
||||
# property, and relationships scores contributed.
|
||||
|
||||
"score_breakdown_enabled": True
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3. Semantic Relationship & Triplet Deduplication
|
||||
|
||||
**The problem:** The legacy relationship deduplication relied on exact `(Subject, Predicate, Object)` string matches. It couldn't recognize that `(Person, "works_for", Company)` is semantically identical to `(Person, "employed_by", Company)` .
|
||||
|
||||
**The V2 Solution:** A new `semantic_v2` mode that introduces predicate synonym mapping, literal normalization (cleaning up rogue spaces/casing), and a highly optimized $O(1)$ canonical hash path for fast matching.
|
||||
|
||||
|
||||
|
||||
**How to Opt-In**
|
||||
|
||||
When calling relationship-specific dedup methods, pass the new configuration keys:
|
||||
|
||||
```python
|
||||
from semantica.deduplication import DuplicateDetector
|
||||
from semantica.deduplication.methods import dedup_triplets
|
||||
|
||||
|
||||
# Approach A: Using the Detector explicitly
|
||||
detector = DuplicateDetector()
|
||||
duplicates = detector.detect_relationship_duplicates(
|
||||
relationship_list,
|
||||
relationship_dedup_mode="semantic_v2",
|
||||
|
||||
# Cleans up messy object strings
|
||||
# (e.g., " Apple Inc. " -> "apple inc.")
|
||||
literal_normalization_enabled=True,
|
||||
|
||||
# Maps various synonyms to a single canonical predicate
|
||||
# before hashing
|
||||
predicate_synonym_map={
|
||||
"works_for": "employed_by",
|
||||
"is_employee_of": "employed_by",
|
||||
"has_employer": "employed_by"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Approach B: Using the new simplified wrapper in methods.py
|
||||
duplicates = dedup_triplets(
|
||||
relationships_list,
|
||||
mode="semantic_v2",
|
||||
literal_normalization_enabled=True,
|
||||
predicate_synonym_map={"works_for": "employed_by"}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
###### Note on Merge Strategies
|
||||
|
||||
When using `semantic_v2` for relationships, the `MergeStrategyManager` will now automatically respect your canonicalized keys. If two entities share a relationship that differs only by a mapped synonym, the engine will correctly identify them as the same relationship and prevent duplicate graph edges during the merge phase.
|
||||
|
||||
|
||||
|
||||
### Need Help?
|
||||
|
||||
If you experience any unexpected behavior when switching from `legacy` to `blocking_v2` or `semantic_v2`, please check the explainability metadata (by setting `"score_breakdown_enabled": True`) to audit the exact scoring process, or open an issue on GitHub.
|
||||
+68
-122
@@ -1,180 +1,126 @@
|
||||
# Architecture
|
||||
|
||||
Semantica's modular, extensible framework for semantic intelligence and knowledge engineering.
|
||||
Semantica is built around a three-layer, modular architecture designed for independent use of components, clean separation of concerns, and extensibility at each layer.
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **Modular**: Independent, reusable components
|
||||
- **Extensible**: Easy to add new functionality
|
||||
- **Scalable**: Handle large-scale data processing
|
||||
- **Maintainable**: Clear separation of concerns
|
||||
|
||||
---
|
||||
|
||||
## System Architecture
|
||||
## System Overview
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Data Ingestion Layer] --> B[Semantic Processing Layer]
|
||||
B --> C[Application Layer]
|
||||
|
||||
A1[Files • Web • APIs • Streams] --> A
|
||||
B1[Parse • Normalize • Extract • Build] --> B
|
||||
C1[GraphRAG • AI Agents • Analytics] --> C
|
||||
|
||||
A1[Files · Web · APIs · Streams] --> A
|
||||
B1[Parse · Normalize · Extract · Build] --> B
|
||||
C1[GraphRAG · AI Agents · Analytics] --> C
|
||||
```
|
||||
|
||||
### Three-Layer Architecture
|
||||
|
||||
**1. Data Ingestion Layer**
|
||||
- Multiple file formats (PDF, DOCX, JSON, CSV, etc.)
|
||||
- Web scraping and APIs
|
||||
- Real-time streams (Kafka, RabbitMQ)
|
||||
- Database connectors (SQL, NoSQL)
|
||||
|
||||
**2. Semantic Processing Layer**
|
||||
- Document parsing and normalization
|
||||
- Entity and relationship extraction
|
||||
- Embedding generation
|
||||
- Knowledge graph construction
|
||||
- Quality assurance and deduplication
|
||||
|
||||
**3. Application Layer**
|
||||
- GraphRAG for enhanced retrieval
|
||||
- AI agent memory and context
|
||||
- Multi-agent systems
|
||||
- Analytics and visualization
|
||||
|
||||
---
|
||||
|
||||
## Core Modules
|
||||
## Three-Layer Architecture
|
||||
|
||||
### Orchestration
|
||||
- **`semantica.core`** - Main framework class and coordination
|
||||
- **`semantica.pipeline`** - Pipeline management and execution
|
||||
### 1. Data Ingestion Layer
|
||||
|
||||
### Data Processing
|
||||
- **`semantica.ingest`** - Universal data ingestion
|
||||
- **`semantica.parse`** - Document parsing
|
||||
- **`semantica.normalize`** - Data cleaning and normalization
|
||||
Responsible for loading data from any source into the pipeline.
|
||||
|
||||
### Semantic Intelligence
|
||||
- **`semantica.semantic_extract`** - Entity and relationship extraction
|
||||
- **`semantica.embeddings`** - Vector embedding generation
|
||||
- **`semantica.ontology`** - Ontology generation and management
|
||||
- **File formats** — PDF, DOCX, HTML, JSON, CSV, Excel, PPTX, archives
|
||||
- **Web** — crawl via `WebIngestor` with configurable depth
|
||||
- **Databases** — SQL, NoSQL, Snowflake via `DBIngestor` / `SnowflakeIngestor`
|
||||
- **Streams** — Kafka, real-time feeds
|
||||
|
||||
### Knowledge Graphs
|
||||
- **`semantica.kg`** - Knowledge graph construction
|
||||
- **`semantica.vector_store`** - Vector storage (Weaviate, FAISS)
|
||||
- **`semantica.triplet_store`** - RDF triplet storage (Jena, Blazegraph)
|
||||
- **`semantica.graph_store`** - Property graphs (Neo4j, FalkorDB)
|
||||
### 2. Semantic Processing Layer
|
||||
|
||||
### Quality Assurance
|
||||
- **`semantica.deduplication`** - Entity deduplication
|
||||
- **`semantica.conflicts`** - Conflict detection and resolution
|
||||
The core intelligence engine — transforms raw data into structured knowledge.
|
||||
|
||||
- Document parsing and normalization
|
||||
- Entity and relationship extraction (NER, LLM-typed, rule-based)
|
||||
- Embedding generation
|
||||
- Knowledge graph construction with entity merging
|
||||
- Deduplication, conflict detection, and validation
|
||||
|
||||
### 3. Application Layer
|
||||
|
||||
Consumes the knowledge graph for downstream use cases.
|
||||
|
||||
- GraphRAG — graph-grounded retrieval for LLMs
|
||||
- AI agent context and decision tracking
|
||||
- Multi-agent pipelines
|
||||
- Analytics, visualization, and export
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
1. Ingestion → Raw data from sources
|
||||
2. Parsing → Structured content extraction
|
||||
3. Normalization → Cleaned data
|
||||
4. Semantic Extraction → Entities, relationships, events
|
||||
5. Graph Construction → Entity resolution, conflict resolution
|
||||
6. Quality Assurance → Deduplication, validation
|
||||
7. Storage → Vector, triplet, and graph stores
|
||||
8. Application → GraphRAG, agents, analytics
|
||||
Ingest → raw data from sources
|
||||
Parse → structured text extraction
|
||||
Normalize → canonical forms, date/name standardization
|
||||
Extract → entities, relationships, events
|
||||
Build → entity resolution, graph construction
|
||||
QA → deduplication, conflict resolution, validation
|
||||
Store → vector store, graph store, triplet store
|
||||
Deliver → GraphRAG, agents, export, visualization
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Map
|
||||
|
||||
| Layer | Modules |
|
||||
|-------|---------|
|
||||
| **Ingestion** | `ingest`, `parse`, `split`, `normalize` |
|
||||
| **Semantic** | `semantic_extract`, `kg`, `ontology`, `reasoning` |
|
||||
| **Storage** | `embeddings`, `vector_store`, `graph_store`, `triplet_store` |
|
||||
| **Quality** | `deduplication`, `conflicts` |
|
||||
| **Context** | `context`, `provenance`, `change_management` |
|
||||
| **Output** | `export`, `visualization`, `pipeline` |
|
||||
|
||||
For full module documentation, see the [Modules Guide](modules.md).
|
||||
|
||||
---
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Custom Ingestors
|
||||
### Custom Ingestor
|
||||
|
||||
```python
|
||||
from semantica.ingest import BaseIngestor
|
||||
|
||||
class CustomIngestor(BaseIngestor):
|
||||
def ingest(self, source):
|
||||
# Custom ingestion logic
|
||||
pass
|
||||
# Return a list of document dicts
|
||||
...
|
||||
```
|
||||
|
||||
### Custom Extractors
|
||||
### Custom Extractor
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract import BaseExtractor
|
||||
|
||||
class CustomExtractor(BaseExtractor):
|
||||
def extract(self, text):
|
||||
# Custom extraction logic
|
||||
pass
|
||||
# Return a list of entity dicts
|
||||
...
|
||||
```
|
||||
|
||||
### Custom Validators
|
||||
|
||||
Validators can be implemented within domain-specific modules (e.g., graph or ontology) as needed.
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Modularity
|
||||
Independent components that can be used standalone or together. Easy to test, maintain, and extend.
|
||||
**Modularity** — every component can be used standalone. Import only what you need; the framework never forces a full stack.
|
||||
|
||||
### Plugin System
|
||||
Extensible architecture allowing custom functionality without modifying core code.
|
||||
**Pluggability** — extend any layer without modifying core code. Custom ingestors, extractors, validators, and exporters all follow the same base class pattern.
|
||||
|
||||
### Configuration Management
|
||||
Centralized configuration with environment variable support for different deployment environments.
|
||||
**Configuration over convention** — centralized config with environment variable overrides for deployment flexibility.
|
||||
|
||||
### Error Handling
|
||||
Comprehensive error handling with graceful degradation and recovery mechanisms.
|
||||
**Provenance by default** — lineage tracking is built into graph construction, not bolted on. Every node traces back to a source document.
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
## Performance Characteristics
|
||||
|
||||
**Scalability**
|
||||
- Parallel processing support
|
||||
- Streaming for large datasets
|
||||
- Efficient memory usage
|
||||
- Intelligent caching
|
||||
|
||||
**Optimization**
|
||||
- Lazy loading
|
||||
- Batch processing
|
||||
- Connection pooling
|
||||
- Query optimization
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
**Data Security**
|
||||
- Secure credential handling
|
||||
- Input validation and output sanitization
|
||||
- Audit logging
|
||||
|
||||
**Access Control**
|
||||
- Authentication and authorization
|
||||
- API key management
|
||||
- Role-based access control
|
||||
|
||||
---
|
||||
|
||||
## Future Roadmap
|
||||
|
||||
- Distributed processing
|
||||
- Real-time streaming improvements
|
||||
- Advanced reasoning capabilities
|
||||
- Multi-modal expansion
|
||||
- Enhanced visualization
|
||||
|
||||
---
|
||||
|
||||
For detailed module documentation, see [Modules Guide](modules.md)
|
||||
- **Parallel execution** — `PipelineBuilder` supports configurable worker counts per stage
|
||||
- **Delta processing** — incremental graph updates without full recompute
|
||||
- **Streaming ingestion** — process large corpora without loading everything into memory
|
||||
- **Backend flexibility** — swap in-memory NetworkX for Neo4j/FalkorDB at scale with no API changes
|
||||
|
||||
+75
-275
@@ -1,372 +1,172 @@
|
||||
# Core Concepts
|
||||
# Core Concepts
|
||||
|
||||
**Learn the fundamental concepts behind Semantica in simple, practical terms.**
|
||||
The fundamental ideas behind Semantica — explained plainly.
|
||||
|
||||
!!! tip "Quick Start"
|
||||
New to Semantica? Start with [Getting Started](getting-started.md) for hands-on examples.
|
||||
!!! tip "New here?"
|
||||
Start with [Getting Started](getting-started.md) for hands-on examples, then come back to this page for deeper understanding.
|
||||
|
||||
---
|
||||
|
||||
## What is Semantica?
|
||||
|
||||
Semantica transforms unstructured data (documents, web pages, reports) into **knowledge graphs** - structured databases that AI systems can understand and reason about.
|
||||
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.
|
||||
|
||||
**What it does:**
|
||||
- **Reads** documents, PDFs, web pages, databases
|
||||
- **Extracts** entities (people, companies, dates) and relationships
|
||||
- **Builds** connected knowledge graphs
|
||||
- **Enables** AI to reason with structured knowledge
|
||||
|
||||
---
|
||||
|
||||
## Core Architecture
|
||||
|
||||
Semantica uses a **layered architecture** - use only what you need:
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- **Input Layer**
|
||||
|
||||
---
|
||||
|
||||
Data ingestion and preparation
|
||||
|
||||
**Modules**: Ingest, Parse, Split, Normalize
|
||||
|
||||
- **Semantic Layer**
|
||||
|
||||
---
|
||||
|
||||
Intelligence and understanding
|
||||
|
||||
**Modules**: Semantic Extract, Knowledge Graph, Ontology, Reasoning
|
||||
|
||||
- **Storage Layer**
|
||||
|
||||
---
|
||||
|
||||
Persistent data storage
|
||||
|
||||
**Modules**: Embeddings, Vector Store, Graph Store
|
||||
|
||||
- **Quality Layer**
|
||||
|
||||
---
|
||||
|
||||
Data quality and consistency
|
||||
|
||||
**Modules**: Deduplication, Conflicts
|
||||
|
||||
- **Context & Memory**
|
||||
|
||||
---
|
||||
|
||||
Agent memory and foundation data
|
||||
|
||||
**Modules**: Context, Seed, LLM Providers
|
||||
|
||||
- **Output & Orchestration**
|
||||
|
||||
---
|
||||
|
||||
Export, visualization, and workflows
|
||||
|
||||
**Modules**: Export, Visualization, Pipeline
|
||||
|
||||
</div>
|
||||
At its core, Semantica adds a **context and intelligence layer** on top of your existing AI stack: it doesn't replace LangChain, LlamaIndex, or your LLM provider — it makes their outputs accountable.
|
||||
|
||||
---
|
||||
|
||||
## Knowledge Graphs
|
||||
|
||||
The foundation of Semantica - turning data into structured knowledge.
|
||||
The foundation of everything in Semantica.
|
||||
|
||||
### What is a Knowledge Graph?
|
||||
A knowledge graph stores information as:
|
||||
|
||||
A knowledge graph represents real-world information as:
|
||||
- **Nodes** (entities): People, companies, locations, dates
|
||||
- **Edges** (relationships): works_for, located_in, founded_by
|
||||
- **Properties**: Name, date, confidence score, source
|
||||
- **Nodes (entities)** — people, companies, locations, events, concepts
|
||||
- **Edges (relationships)** — `works_for`, `located_in`, `founded_by`
|
||||
- **Properties** — name, date, confidence score, source URL
|
||||
|
||||
### Why Knowledge Graphs?
|
||||
|
||||
- **Searchable**: Find information instantly
|
||||
- **Connectable**: Discover hidden relationships
|
||||
- **Queryable**: Ask complex questions
|
||||
- **Explainable**: Trace answers back to sources
|
||||
This structure makes knowledge **searchable**, **connectable**, **queryable**, and — critically — **explainable**: every answer can be traced back to the facts and relationships that produced it.
|
||||
|
||||
---
|
||||
|
||||
## Entity Extraction (NER)
|
||||
|
||||
Finding and classifying entities in text.
|
||||
Scanning text to find and classify real-world entities.
|
||||
|
||||
### What it does:
|
||||
- Scans text for people, organizations, locations, dates
|
||||
- Classifies each entity by type
|
||||
- Assigns confidence scores
|
||||
- Tracks source provenance
|
||||
|
||||
### Example Output:
|
||||
```python
|
||||
# From: "Apple Inc. was founded by Steve Jobs in 1976 in Cupertino."
|
||||
# Input: "Apple Inc. was founded by Steve Jobs in 1976 in Cupertino."
|
||||
{
|
||||
"entities": [
|
||||
{"text": "Apple Inc.", "type": "ORGANIZATION", "confidence": 0.98},
|
||||
{"text": "Steve Jobs", "type": "PERSON", "confidence": 0.99},
|
||||
{"text": "1976", "type": "DATE", "confidence": 0.95},
|
||||
{"text": "Cupertino", "type": "LOCATION", "confidence": 0.97}
|
||||
{"text": "Apple Inc.", "type": "ORGANIZATION", "confidence": 0.98},
|
||||
{"text": "Steve Jobs", "type": "PERSON", "confidence": 0.99},
|
||||
{"text": "1976", "type": "DATE", "confidence": 0.95},
|
||||
{"text": "Cupertino", "type": "LOCATION", "confidence": 0.97}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each entity gets a type, confidence score, and a link to its source document.
|
||||
|
||||
---
|
||||
|
||||
## Relationship Extraction
|
||||
|
||||
Finding connections between entities.
|
||||
Finding how entities connect to each other.
|
||||
|
||||
### What it does:
|
||||
- Identifies how entities relate to each other
|
||||
- Extracts relationship types and directions
|
||||
- Provides context and confidence
|
||||
- Links to source documents
|
||||
|
||||
### Example Output:
|
||||
```python
|
||||
{
|
||||
"relationships": [
|
||||
{"subject": "Steve Jobs", "predicate": "founded", "object": "Apple Inc.", "confidence": 0.92},
|
||||
{"subject": "Apple Inc.", "predicate": "located_in", "object": "Cupertino", "confidence": 0.89}
|
||||
{"subject": "Steve Jobs", "predicate": "founded", "object": "Apple Inc.", "confidence": 0.92},
|
||||
{"subject": "Apple Inc.", "predicate": "located_in", "object": "Cupertino", "confidence": 0.89}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Relationships can be extracted via rule-based methods, ML models, or LLMs (with `"llm_typed"` metadata).
|
||||
|
||||
---
|
||||
|
||||
## Embeddings
|
||||
|
||||
Turning text into numerical vectors for AI understanding.
|
||||
Embeddings convert text into numerical vectors so that AI systems can measure semantic similarity — finding related concepts even when the exact words differ.
|
||||
|
||||
### What are embeddings?
|
||||
- **Numerical representations** of text, entities, and relationships
|
||||
- **Similarity calculations** - find related concepts
|
||||
- **AI-powered search** - semantic understanding
|
||||
- **Clustering and grouping** - discover patterns
|
||||
Semantica uses embeddings for:
|
||||
|
||||
### Use Cases:
|
||||
- **Semantic Search** - find documents by meaning, not keywords
|
||||
- **Entity Resolution** - match similar entities across sources
|
||||
- **Recommendations** - suggest related content
|
||||
- **AI Input** - provide structured context to LLMs
|
||||
|
||||
---
|
||||
|
||||
## Temporal Graphs
|
||||
|
||||
Knowledge graphs that understand time.
|
||||
|
||||
### What they track:
|
||||
- **When** events happened
|
||||
- **How** entities changed over time
|
||||
- **Temporal relationships** - before, after, during
|
||||
- **Historical context** - point-in-time snapshots
|
||||
|
||||
### Example Uses:
|
||||
- **Company History** - track mergers, leadership changes
|
||||
- **Person Careers** - job changes, relocations
|
||||
- **Policy Evolution** - law changes over time
|
||||
- **Research Progress** - scientific discoveries timeline
|
||||
- **Semantic search** — retrieve by meaning, not just keywords
|
||||
- **Entity resolution** — match the same entity across different sources
|
||||
- **Precedent search** — find similar past decisions
|
||||
- **GraphRAG retrieval** — hybrid vector + graph traversal
|
||||
|
||||
---
|
||||
|
||||
## GraphRAG
|
||||
|
||||
Enhanced AI retrieval using knowledge graphs.
|
||||
GraphRAG (Graph-Augmented Retrieval Augmented Generation) enhances LLM responses by grounding them in a structured knowledge graph rather than raw text chunks alone.
|
||||
|
||||
### How it works:
|
||||
1. **Query** user question
|
||||
2. **Retrieve** relevant graph context
|
||||
3. **Enhance** with relationships and entities
|
||||
4. **Generate** AI response with sources
|
||||
How it works:
|
||||
|
||||
### Benefits:
|
||||
- **More accurate** answers
|
||||
- **Source attribution** - trace answers back
|
||||
- **Context awareness** - understand relationships
|
||||
- **Reduced hallucination** - grounded in facts
|
||||
1. User submits a query
|
||||
2. Semantica retrieves relevant graph context (entities, relationships, reasoning paths)
|
||||
3. The LLM generates a response grounded in that context
|
||||
4. Every claim in the response links back to a source node in the graph
|
||||
|
||||
This eliminates the hallucination and traceability problems of standard RAG.
|
||||
|
||||
---
|
||||
|
||||
## Ontology
|
||||
|
||||
Defining the structure and rules of your knowledge.
|
||||
An ontology defines the schema and rules for your knowledge — what entity types exist, which relationships are valid, and what constraints apply.
|
||||
|
||||
### What it provides:
|
||||
- **Schema definition** - what types exist
|
||||
- **Relationship rules** - valid connections
|
||||
- **Property constraints** - required fields
|
||||
- **Inheritance hierarchies** - parent-child relationships
|
||||
|
||||
### Example:
|
||||
```python
|
||||
# Define ontology structure
|
||||
ontology = {
|
||||
"classes": ["Person", "Organization", "Location"],
|
||||
"properties": ["name", "date", "confidence"],
|
||||
"relationships": ["works_for", "located_in", "born_in"],
|
||||
"relationships": ["works_for", "located_in", "founded_by"],
|
||||
"rules": {
|
||||
"Person": ["must_have_name", "can_have_birth_date"],
|
||||
"Person": ["must_have_name"],
|
||||
"Organization": ["must_have_name", "can_have_founding_date"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Semantica can auto-generate ontologies from your knowledge graph, or import existing OWL/RDF/Turtle ontologies.
|
||||
|
||||
---
|
||||
|
||||
## Reasoning & Inference
|
||||
|
||||
Making logical deductions from your knowledge.
|
||||
Semantica includes multiple reasoning engines to derive new knowledge from existing facts.
|
||||
|
||||
### What it can do:
|
||||
- **Infer missing facts** - derive new knowledge
|
||||
- **Detect inconsistencies** - find contradictions
|
||||
- **Apply rules** - automate decision making
|
||||
- **Explain reasoning** - show how conclusions were reached
|
||||
```
|
||||
Known: Steve Jobs founded Apple Inc.
|
||||
Known: Apple Inc. is headquartered in Cupertino
|
||||
Inferred: Steve Jobs has a connection to Cupertino
|
||||
```
|
||||
|
||||
### Example:
|
||||
```
|
||||
Known: Steve Jobs founded Apple Inc.
|
||||
Known: Apple Inc. is headquartered in Cupertino
|
||||
Inferred: Steve Jobs has connection to Cupertino
|
||||
```
|
||||
Supported engines: forward chaining, Rete network, deductive, abductive, and SPARQL reasoning — all producing **explainable inference paths**, not black-box conclusions.
|
||||
|
||||
---
|
||||
|
||||
## Temporal Graphs
|
||||
|
||||
Knowledge changes over time. Temporal graphs attach `valid_from` / `valid_until` windows to nodes and edges, enabling point-in-time queries and historical analysis.
|
||||
|
||||
Common uses: tracking company leadership changes, policy evolution, research timelines, financial instrument histories.
|
||||
|
||||
---
|
||||
|
||||
## Deduplication & Entity Resolution
|
||||
|
||||
Finding and merging duplicate entities.
|
||||
Real-world data contains the same entity under many names — "Apple", "Apple Inc.", "Apple Computer Inc." Semantica's deduplication pipeline detects these, merges attributes, resolves conflicts, and preserves the original source provenance.
|
||||
|
||||
### What it does:
|
||||
- **Detects duplicates** - same entity, different names
|
||||
- **Merges information** - combine attributes
|
||||
- **Resolves conflicts** - handle contradictory data
|
||||
- **Maintains provenance** - track original sources
|
||||
|
||||
### Example:
|
||||
```python
|
||||
# These refer to the same entity:
|
||||
"Apple Inc." → "Apple" → "Apple Computer Inc."
|
||||
# Merge into single entity with all attributes
|
||||
```
|
||||
Strategies: Jaro-Winkler similarity (v1), `blocking_v2`, `hybrid_v2`, `semantic_v2` (v2 — up to 7x faster).
|
||||
|
||||
---
|
||||
|
||||
## Data Normalization
|
||||
## Provenance & Auditability
|
||||
|
||||
Cleaning and standardizing your data.
|
||||
Every fact in Semantica links back to:
|
||||
|
||||
### What it fixes:
|
||||
- **Format inconsistencies** - dates, names, numbers
|
||||
- **Canonical forms** - standard representations
|
||||
- **Data quality** - remove errors and noise
|
||||
- **Standardization** - consistent naming conventions
|
||||
- The source document it came from
|
||||
- The extraction method used
|
||||
- The ontology rules applied
|
||||
- The reasoning steps that produced any inference
|
||||
|
||||
### Examples:
|
||||
- **Dates**: "Jan 1, 2020" → "2020-01-01"
|
||||
- **Names**: "Dr. Smith PhD" → "John Smith"
|
||||
- **Companies**: "Apple" → "Apple Inc."
|
||||
- **Locations**: "NYC" → "New York City"
|
||||
This is W3C PROV-O compliant lineage — suitable for regulated industries that require audit trails.
|
||||
|
||||
---
|
||||
|
||||
## Conflict Detection
|
||||
|
||||
Finding and resolving contradictory information.
|
||||
When multiple sources disagree on the same fact, Semantica flags and resolves the conflict rather than silently picking one value.
|
||||
|
||||
### What it identifies:
|
||||
- **Factual conflicts** - different values for same fact
|
||||
- **Temporal conflicts** - impossible timelines
|
||||
- **Logical conflicts** - contradictory relationships
|
||||
- **Source reliability** - trustworthiness assessment
|
||||
|
||||
### Resolution Strategies:
|
||||
- **Most recent** - prefer newer information
|
||||
- **Most reliable** - prefer trusted sources
|
||||
- **Majority vote** - go with consensus
|
||||
- **Manual review** - flag for human review
|
||||
Resolution strategies: prefer most recent, prefer most reliable source, majority vote, or flag for manual review.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
## Next Steps
|
||||
|
||||
Ready to build your first knowledge graph?
|
||||
|
||||
### Quick Start (5 minutes)
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
# Extract entities
|
||||
ner = NERExtractor()
|
||||
entities = ner.extract("Apple Inc. was founded by Steve Jobs in 1976.")
|
||||
|
||||
# Build graph
|
||||
kg = GraphBuilder().build({"entities": entities, "relationships": []})
|
||||
```
|
||||
|
||||
### Learn More
|
||||
- **Getting Started Guide** - [Getting Started](getting-started.md)
|
||||
- **Cookbook Examples** - [Cookbook](cookbook.md)
|
||||
- **Module Documentation** - [Reference](reference/)
|
||||
- **Community Support** - [Community](community.md)
|
||||
|
||||
### Common Use Cases
|
||||
- **Document Analysis** - extract knowledge from reports
|
||||
- **Research Assistant** - find connections in academic papers
|
||||
- **Business Intelligence** - analyze company relationships
|
||||
- **Regulatory Compliance** - track policy changes
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Start Small
|
||||
- Begin with a single document type
|
||||
- Focus on specific entity types
|
||||
- Validate results before scaling
|
||||
|
||||
### Configure Properly
|
||||
- Choose appropriate models for your domain
|
||||
- Set confidence thresholds
|
||||
- Define clear ontology rules
|
||||
|
||||
### Validate Data
|
||||
- Check extraction quality
|
||||
- Review relationship accuracy
|
||||
- Test with known examples
|
||||
|
||||
### Handle Errors
|
||||
- Implement error handling
|
||||
- Log processing issues
|
||||
- Provide feedback mechanisms
|
||||
|
||||
### Optimize Performance
|
||||
- Use appropriate storage backends
|
||||
- Cache frequently accessed data
|
||||
- Monitor resource usage
|
||||
|
||||
### Document Workflows
|
||||
- Record processing steps
|
||||
- Track data sources
|
||||
- Maintain change logs
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Documentation**: [Getting Started](getting-started.md)
|
||||
- **Examples**: [Cookbook](cookbook.md)
|
||||
- **Community**: [Discord](community.md)
|
||||
- **Issues**: [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)
|
||||
- **Support**: [Contact Us](community.md)
|
||||
- [Quickstart Tutorial](quickstart.md) — build a full pipeline with code
|
||||
- [Modules Guide](modules.md) — every module explained
|
||||
- [Use Cases](use-cases.md) — real-world domain examples
|
||||
- [API Reference](reference/core.md) — complete technical reference
|
||||
|
||||
+54
-85
@@ -1,130 +1,99 @@
|
||||
# Contributing
|
||||
# Contributing
|
||||
|
||||
**Help us build Semantica! Every contribution makes the project better.**
|
||||
Contributions of all kinds are welcome — code, documentation, tests, and community support.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
## Quick Start
|
||||
|
||||
### Quick Start
|
||||
1. **Fork** the repository
|
||||
2. **Create** a feature branch
|
||||
3. **Make** your changes
|
||||
4. **Test** your changes
|
||||
5. **Submit** a pull request
|
||||
```bash
|
||||
# Fork the repo, then:
|
||||
git clone https://github.com/your-username/semantica.git
|
||||
cd semantica
|
||||
pip install -e ".[dev]"
|
||||
pytest
|
||||
```
|
||||
|
||||
### First Contribution?
|
||||
Look for issues labeled [`good-first-issue`](https://github.com/Hawksight-AI/semantica/labels/good-first-issue) for beginner-friendly tasks.
|
||||
First time? Look for [`good-first-issue`](https://github.com/Hawksight-AI/semantica/labels/good-first-issue) labels for beginner-friendly tasks.
|
||||
|
||||
---
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
### Code
|
||||
- **Fix bugs** - Resolve reported issues
|
||||
- **Add features** - Implement new functionality
|
||||
- **Improve performance** - Optimize existing code
|
||||
- **Refactor** - Clean up code structure
|
||||
**Code**
|
||||
- Fix bugs and resolve open issues
|
||||
- Implement new features or integrations
|
||||
- Optimize performance or refactor existing code
|
||||
|
||||
### Documentation
|
||||
- **Fix typos** - Correct spelling and grammar
|
||||
- **Improve guides** - Make documentation clearer
|
||||
- **Add examples** - Provide practical code examples
|
||||
- **Update API docs** - Keep reference current
|
||||
**Documentation**
|
||||
- Fix typos, improve clarity, add examples
|
||||
- Write tutorials or domain-specific cookbook notebooks
|
||||
- Keep API reference up to date
|
||||
|
||||
### Testing
|
||||
- **Write tests** - Add test coverage
|
||||
- **Fix tests** - Resolve test failures
|
||||
- **Report issues** - Identify bugs through testing
|
||||
**Testing**
|
||||
- Add test coverage for untested modules
|
||||
- Reproduce and confirm reported bugs
|
||||
- Improve test reliability
|
||||
|
||||
### Community
|
||||
- **Help others** - Answer questions in issues
|
||||
- **Share knowledge** - Write tutorials and guides
|
||||
- **Provide feedback** - Review pull requests
|
||||
**Community**
|
||||
- Answer questions in issues and discussions
|
||||
- Review pull requests
|
||||
- Share Semantica in your blog posts or talks
|
||||
|
||||
---
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
### Bug Reports
|
||||
When reporting bugs, include:
|
||||
- **Description** - What happened
|
||||
- **Steps to reproduce** - How to trigger the issue
|
||||
- **Expected behavior** - What should happen
|
||||
- **Environment** - Your setup details
|
||||
|
||||
Include: what happened, steps to reproduce, expected behavior, and your environment (Python version, OS, Semantica version).
|
||||
|
||||
### Feature Requests
|
||||
When suggesting features, include:
|
||||
- **Use case** - Why you need this feature
|
||||
- **Proposed solution** - How it should work
|
||||
- **Benefits** - How it helps the community
|
||||
|
||||
Include: your use case, what you'd like Semantica to do, and how it benefits others.
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
### Before Submitting
|
||||
- **Test** your changes thoroughly
|
||||
- **Document** new features with examples
|
||||
- **Update** relevant documentation
|
||||
- **Follow** the existing code style
|
||||
Before submitting:
|
||||
|
||||
### Pull Request Checklist
|
||||
- [ ] Code follows project style
|
||||
- [ ] Tests pass locally
|
||||
- [ ] Documentation is updated
|
||||
- [ ] Commit messages are clear
|
||||
- [ ] No merge conflicts
|
||||
- [ ] Tests pass locally (`pytest`)
|
||||
- [ ] New features are documented with examples
|
||||
- [ ] Code follows project style (Black, isort, flake8)
|
||||
- [ ] Commit messages are clear and descriptive
|
||||
- [ ] No unresolved merge conflicts
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Clone your fork
|
||||
git clone https://github.com/your-username/semantica.git
|
||||
cd semantica
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
# Install in development mode
|
||||
pip install -e .[dev]
|
||||
Code style tools used: **Black** (formatting), **isort** (imports), **flake8** (linting).
|
||||
|
||||
# Run tests
|
||||
Run the full test suite:
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
### Code Style
|
||||
We use standard Python formatting:
|
||||
- **Black** for code formatting
|
||||
- **isort** for import sorting
|
||||
- **flake8** for linting
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
Please follow the [Code of Conduct](https://github.com/Hawksight-AI/semantica/blob/main/CODE_OF_CONDUCT.md). Be respectful, patient, and constructive.
|
||||
|
||||
All contributors are recognized in release notes and the GitHub contributors list.
|
||||
|
||||
---
|
||||
|
||||
## Community Guidelines
|
||||
## Help
|
||||
|
||||
### Code of Conduct
|
||||
Please follow our [Code of Conduct](https://github.com/Hawksight-AI/semantica/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
### Communication
|
||||
- **Be respectful** - Treat everyone with kindness
|
||||
- **Be helpful** - Assist others when you can
|
||||
- **Be patient** - Allow time for reviews
|
||||
- **Be constructive** - Provide helpful feedback
|
||||
|
||||
---
|
||||
|
||||
## Recognition
|
||||
|
||||
All contributors are recognized in:
|
||||
- **GitHub contributors** - Automatic recognition
|
||||
- **Release notes** - Notable contributions
|
||||
- **Community highlights** - Outstanding work
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **[GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)** - Ask questions
|
||||
- **[Discussions](https://github.com/Hawksight-AI/semantica/discussions)** - Community chat
|
||||
- **[Code of Conduct](https://github.com/Hawksight-AI/semantica/blob/main/CODE_OF_CONDUCT.md)** - Community standards
|
||||
- [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)
|
||||
- [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
|
||||
- [Discord](https://discord.gg/sV34vps5hH)
|
||||
|
||||
+35
-85
@@ -1,108 +1,58 @@
|
||||
# 🍳 Semantica Cookbook
|
||||
# Semantica Cookbook
|
||||
|
||||
Welcome to the **Semantica Cookbook**!
|
||||
Interactive Jupyter notebooks covering everything from your first knowledge graph to production GraphRAG systems.
|
||||
|
||||
This collection of Jupyter notebooks is designed to take you from a beginner to an expert in building semantic AI applications. Whether you're looking for quick recipes or deep-dive tutorials, you'll find it here.
|
||||
|
||||
!!! tip "How to use this Cookbook"
|
||||
- **Beginners**: Start with the [Core Tutorials](#core-tutorials) to learn the basics.
|
||||
- **Developers**: Check out [Advanced Concepts](#advanced-concepts) for deep dives into specific features.
|
||||
- **Architects**: Explore [Industry Use Cases](#industry-use-cases) for end-to-end solutions.
|
||||
!!! tip "Where to start"
|
||||
- **New to Semantica** — begin with [Core Tutorials](#core-tutorials)
|
||||
- **Building an application** — see [Advanced Concepts](#advanced-concepts) or [Industry Use Cases](#industry-use-cases)
|
||||
- **Need installation help** — see the [Installation Guide](installation.md)
|
||||
|
||||
!!! note "Prerequisites"
|
||||
Before running these notebooks, ensure you have:
|
||||
- Python 3.8+ installed
|
||||
- A basic understanding of Python and Jupyter
|
||||
- An OpenAI API key (for most examples)
|
||||
|
||||
!!! success "Installation"
|
||||
Install Semantica from PyPI (recommended):
|
||||
|
||||
```bash
|
||||
pip install semantica
|
||||
# Or with all optional dependencies:
|
||||
pip install semantica[all]
|
||||
```
|
||||
|
||||
For more installation options, see the [Installation Guide](installation.md).
|
||||
Python 3.8+, Jupyter, and an OpenAI API key (for most examples).
|
||||
|
||||
---
|
||||
|
||||
## � Featured Recipes
|
||||
|
||||
Hand-picked tutorials to show you the power of Semantica.
|
||||
## Featured Recipes
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-robot: **GraphRAG Complete**
|
||||
---
|
||||
Build a production-ready Graph Retrieval Augmented Generation system.
|
||||
|
||||
**Topics**: RAG, LLMs, Vector Search, Graph Traversal
|
||||
|
||||
**Difficulty**: Advanced
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)
|
||||
|
||||
- :material-scale-balance: **RAG vs. GraphRAG Comparison**
|
||||
---
|
||||
Side-by-side comparison of Standard RAG vs. GraphRAG using real-world data.
|
||||
|
||||
**Topics**: RAG, GraphRAG, Benchmarking, Visualization
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)
|
||||
|
||||
- :material-robot: **GraphRAG Complete**
|
||||
---
|
||||
Build a production-ready Graph Retrieval Augmented Generation system.
|
||||
|
||||
**New Features**: Graph Validation, Logical Inference, Hybrid Context.
|
||||
|
||||
**Topics**: RAG, LLMs, Vector Search, Graph Traversal
|
||||
|
||||
**Difficulty**: Advanced
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)
|
||||
|
||||
- :material-scale-balance: **RAG vs. GraphRAG Comparison**
|
||||
---
|
||||
Side-by-side comparison of Standard RAG vs. GraphRAG using real-world data.
|
||||
|
||||
**New Features**: Inference-Enhanced GraphRAG, Reasoning Gap Analysis.
|
||||
|
||||
**Topics**: RAG, GraphRAG, Benchmarking, Visualization
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)
|
||||
|
||||
- :material-graph: **Your First Knowledge Graph**
|
||||
---
|
||||
Go from raw text to a queryable knowledge graph in 20 minutes.
|
||||
|
||||
**Topics**: Extraction, Graph Construction, Visualization
|
||||
|
||||
**Difficulty**: Beginner
|
||||
|
||||
|
||||
**Topics**: Extraction, Graph Construction, Visualization · **Difficulty**: Beginner
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)
|
||||
|
||||
- :material-robot: **GraphRAG Complete**
|
||||
---
|
||||
Build a production-ready Graph Retrieval Augmented Generation system with hybrid retrieval and logical inference.
|
||||
|
||||
**Topics**: RAG, LLMs, Vector Search, Graph Traversal · **Difficulty**: Advanced
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)
|
||||
|
||||
- :material-scale-balance: **RAG vs. GraphRAG Comparison**
|
||||
---
|
||||
Side-by-side benchmark of standard RAG vs. GraphRAG on real-world data.
|
||||
|
||||
**Topics**: RAG, GraphRAG, Benchmarking, Reasoning Gap · **Difficulty**: Intermediate
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)
|
||||
|
||||
- :material-shield-alert: **Real-Time Anomaly Detection**
|
||||
---
|
||||
Detect anomalies in streaming data using dynamic graphs.
|
||||
|
||||
**Topics**: Streaming, Security, Dynamic Graphs
|
||||
|
||||
**Difficulty**: Advanced
|
||||
|
||||
Detect anomalies in streaming data using dynamic knowledge graphs.
|
||||
|
||||
**Topics**: Streaming, Security, Dynamic Graphs · **Difficulty**: Advanced
|
||||
|
||||
[Open Notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/cybersecurity/01_Real_Time_Anomaly_Detection.ipynb)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🏁 Core Tutorials {#core-tutorials}
|
||||
## Core Tutorials {#core-tutorials}
|
||||
|
||||
Essential guides to master the Semantica framework.
|
||||
|
||||
@@ -210,7 +160,7 @@ Essential guides to master the Semantica framework.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Advanced Concepts
|
||||
## Advanced Concepts
|
||||
|
||||
Deep dive into advanced features, customization, and complex workflows.
|
||||
|
||||
@@ -329,7 +279,7 @@ Deep dive into advanced features, customization, and complex workflows.
|
||||
|
||||
---
|
||||
|
||||
## 🏭 Industry Use Cases {#industry-use-cases}
|
||||
## Industry Use Cases {#industry-use-cases}
|
||||
|
||||
Real-world examples and end-to-end applications across various industries.
|
||||
|
||||
@@ -497,7 +447,7 @@ Real-world examples and end-to-end applications across various industries.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ How to Run
|
||||
## How to Run
|
||||
|
||||
To run these notebooks locally:
|
||||
|
||||
|
||||
+66
-50
@@ -216,76 +216,92 @@ html {
|
||||
border-left: 2px solid var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
==========================================================================
|
||||
Layout Optimization
|
||||
==========================================================================
|
||||
==========================================================================
|
||||
*/
|
||||
/* Reduce spacing between sidebars and content for all pages */
|
||||
.md-content__inner {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.md-content {
|
||||
margin-left: 0;
|
||||
/* Widen the overall grid */
|
||||
.md-grid {
|
||||
max-width: 1440px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Reduce spacing before left sidebar for all pages */
|
||||
.md-sidebar {
|
||||
padding-left: 0.25rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Narrow left sidebar to give content more room */
|
||||
.md-sidebar--primary {
|
||||
padding-right: 0.5rem;
|
||||
width: 11rem;
|
||||
padding-right: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* Right TOC sidebar */
|
||||
.md-sidebar--secondary {
|
||||
width: 11rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Reduce spacing after right sidebar (table of contents) and shift it right slightly */
|
||||
.md-sidebar--secondary {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
margin-left: 3.5rem;
|
||||
.md-sidebar--secondary .md-nav {
|
||||
width: 11rem;
|
||||
}
|
||||
|
||||
/* Reduce right edge spacing - similar to left */
|
||||
.md-container {
|
||||
padding-right: 0;
|
||||
/* Tighten TOC list spacing */
|
||||
.md-sidebar--secondary .md-nav__list {
|
||||
padding-bottom: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.md-sidebar--secondary .md-nav__item {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.md-sidebar--secondary .md-nav__link {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
padding-top: 0.15rem;
|
||||
padding-bottom: 0.15rem;
|
||||
line-height: 1.4;
|
||||
font-size: 0.7rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Nested TOC items (h3, h4) */
|
||||
.md-sidebar--secondary .md-nav__item .md-nav__item .md-nav__link {
|
||||
padding-left: 0.6rem;
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
/* Remove extra gap between TOC title and first item */
|
||||
.md-sidebar--secondary .md-nav__title {
|
||||
margin-bottom: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Give the main content area maximum available width */
|
||||
.md-content {
|
||||
max-width: none;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.md-content__inner {
|
||||
max-width: none;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.md-main {
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Reduce margins of the main container */
|
||||
.md-main__inner {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Reduce right edge spacing on body/html */
|
||||
body {
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.md-grid {
|
||||
margin-left: 0;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Ensure text content is left-aligned by default */
|
||||
|
||||
+118
-166
@@ -1,10 +1,15 @@
|
||||
# Deep Dive
|
||||
|
||||
Advanced topics, architecture, and internals of Semantica.
|
||||
Internals, advanced concepts, and extension points for contributors and power users.
|
||||
|
||||
## Architecture Overview
|
||||
!!! tip "Just getting started?"
|
||||
Read [Architecture](architecture.md) for a higher-level overview first.
|
||||
|
||||
Semantica follows a modular, extensible architecture:
|
||||
---
|
||||
|
||||
## Pipeline Internals
|
||||
|
||||
The full data flow through a Semantica pipeline:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
@@ -16,65 +21,79 @@ graph TB
|
||||
F --> G[Knowledge Graph Builder]
|
||||
G --> H[Embedding Generator]
|
||||
H --> I[Export Layer]
|
||||
|
||||
|
||||
D --> D1[Entity Extractor]
|
||||
D --> D2[Relationship Extractor]
|
||||
D --> D3[Triplet Extractor]
|
||||
|
||||
|
||||
G --> G1[Graph Validator]
|
||||
G --> G2[Graph Analyzer]
|
||||
|
||||
|
||||
H --> H1[Text Embeddings]
|
||||
H --> H2[Graph Embeddings]
|
||||
```
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Semantica
|
||||
participant Ingestor
|
||||
participant Parser
|
||||
participant Extractor
|
||||
participant Resolver
|
||||
participant GraphBuilder
|
||||
participant Exporter
|
||||
|
||||
User->>Semantica: build_knowledge_base(sources)
|
||||
Semantica->>Ingestor: ingest(sources)
|
||||
Ingestor->>Parser: parse(documents)
|
||||
Parser->>Extractor: extract(text)
|
||||
Extractor->>Resolver: resolve_conflicts(entities)
|
||||
Resolver->>GraphBuilder: build_graph(resolved_data)
|
||||
GraphBuilder->>Exporter: export(graph)
|
||||
Exporter->>User: return result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Components
|
||||
|
||||
### 1. Ingestion Layer
|
||||
### Ingestion Layer
|
||||
|
||||
Handles data input from various sources:
|
||||
Handles input from any source:
|
||||
|
||||
- **File Ingestor**: PDF, DOCX, HTML, JSON, CSV
|
||||
- **Web Ingestor**: URLs, web scraping
|
||||
- **Database Ingestor**: SQL databases
|
||||
- **Stream Ingestor**: Real-time data streams
|
||||
- **FileIngestor** — PDF, DOCX, HTML, JSON, CSV, archives
|
||||
- **WebIngestor** — URL crawling and scraping
|
||||
- **DBIngestor** / **SnowflakeIngestor** — SQL databases
|
||||
- **StreamIngestor** — Kafka and real-time feeds
|
||||
|
||||
### 2. Parsing Layer
|
||||
### Parsing Layer
|
||||
|
||||
Converts raw data into structured format:
|
||||
Converts raw data to structured text:
|
||||
|
||||
- Document parsing (PDF, Word, etc.)
|
||||
- Text extraction
|
||||
- Metadata extraction
|
||||
- Format normalization
|
||||
- Text and metadata extraction from documents
|
||||
- OCR for scanned content
|
||||
- Layout analysis (via Docling for tables and columns)
|
||||
|
||||
### 3. Extraction Layer
|
||||
### Extraction Layer
|
||||
|
||||
Core semantic extraction:
|
||||
Core semantic processing pipeline:
|
||||
|
||||
```python
|
||||
# Entity extraction pipeline
|
||||
```
|
||||
text → Tokenization → NER → Entity Linking → Entity Validation
|
||||
```
|
||||
|
||||
**Components:**
|
||||
- Named Entity Recognition (NER)
|
||||
- Relationship Extraction
|
||||
- Triplet Extraction
|
||||
- Coreference Resolution
|
||||
Components: Named Entity Recognition, Relationship Extraction, Triplet Extraction, Coreference Resolution.
|
||||
|
||||
### 4. Normalization Layer
|
||||
### Normalization Layer
|
||||
|
||||
Standardizes extracted data:
|
||||
Standardizes extracted data: entity names, date formats, numbers, encodings, and language normalization.
|
||||
|
||||
- Entity normalization
|
||||
- Date/time normalization
|
||||
- Number normalization
|
||||
- Text cleaning
|
||||
### Conflict Resolution
|
||||
|
||||
### 5. Conflict Resolution
|
||||
|
||||
Handles conflicting information:
|
||||
Handles contradictory facts from multiple sources:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
@@ -88,64 +107,28 @@ graph LR
|
||||
E --> H
|
||||
F --> H
|
||||
G --> H
|
||||
|
||||
style A fill:#ffebee
|
||||
style H fill:#c8e6c9
|
||||
style C fill:#fff9c4
|
||||
```
|
||||
|
||||
### 6. Knowledge Graph Builder
|
||||
### Knowledge Graph Builder
|
||||
|
||||
Constructs the knowledge graph:
|
||||
- Entity resolution across sources
|
||||
- Edge creation (typed relationships)
|
||||
- Property assignment with confidence scores
|
||||
- Graph validation and quality checks
|
||||
|
||||
- Node creation (entities)
|
||||
- Edge creation (relationships)
|
||||
- Property assignment
|
||||
- Graph validation
|
||||
- Quality checks
|
||||
### Embedding Generator
|
||||
|
||||
### 7. Embedding Generator
|
||||
- Text embeddings (Sentence-Transformers, FastEmbed, OpenAI, BGE)
|
||||
- Graph embeddings (Node2Vec, GraphSAGE)
|
||||
|
||||
Generates vector representations:
|
||||
|
||||
- Text embeddings (sentence transformers)
|
||||
- Graph embeddings (node2vec, GraphSAGE)
|
||||
- Multimodal embeddings
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Semantica
|
||||
participant Ingestor
|
||||
participant Parser
|
||||
participant Extractor
|
||||
participant Resolver
|
||||
participant GraphBuilder
|
||||
participant Exporter
|
||||
|
||||
User->>Semantica: build_knowledge_base(sources)
|
||||
Semantica->>Ingestor: ingest(sources)
|
||||
Ingestor->>Parser: parse(documents)
|
||||
Parser->>Extractor: extract(text)
|
||||
Extractor->>Resolver: resolve_conflicts(entities)
|
||||
Resolver->>GraphBuilder: build_graph(resolved_data)
|
||||
GraphBuilder->>Exporter: export(graph)
|
||||
Exporter->>User: return result
|
||||
|
||||
Note over User,Exporter: Complete pipeline execution
|
||||
```
|
||||
---
|
||||
|
||||
## Advanced Concepts
|
||||
|
||||
### Entity Resolution
|
||||
|
||||
Matching entities across sources:
|
||||
### Entity Resolution Algorithm
|
||||
|
||||
```python
|
||||
# Entity resolution algorithm
|
||||
def resolve_entities(entities):
|
||||
def resolve_entities(entities, threshold=0.85):
|
||||
clusters = []
|
||||
for entity in entities:
|
||||
matched = False
|
||||
@@ -161,123 +144,92 @@ def resolve_entities(entities):
|
||||
|
||||
### Relationship Inference
|
||||
|
||||
Inferring implicit relationships:
|
||||
Semantica's reasoning engines can derive implicit relationships:
|
||||
|
||||
- Transitive relationships
|
||||
- Temporal relationships
|
||||
- Causal relationships
|
||||
- Hierarchical relationships
|
||||
- **Transitive** — if A→B and B→C, infer A→C
|
||||
- **Temporal** — before, after, during from timestamped facts
|
||||
- **Causal** — IF/THEN rules via `Reasoner`
|
||||
- **Hierarchical** — subclass/instance inference via `OntologyReasoner`
|
||||
|
||||
### Graph Optimization
|
||||
|
||||
Optimizing knowledge graph structure:
|
||||
|
||||
- Node deduplication
|
||||
- Edge consolidation
|
||||
- Path compression
|
||||
- Index optimization
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Scalability
|
||||
|
||||
- **Horizontal Scaling**: Process multiple documents in parallel
|
||||
- **Vertical Scaling**: Use GPU acceleration
|
||||
- **Caching**: Cache embeddings and parsed documents
|
||||
- **Lazy Loading**: Load components on demand
|
||||
|
||||
### Memory Management
|
||||
### Batch Processing for Large Datasets
|
||||
|
||||
```python
|
||||
# Process large datasets efficiently
|
||||
def process_large_dataset(sources, batch_size=100):
|
||||
for i in range(0, len(sources), batch_size):
|
||||
batch = sources[i:i+batch_size]
|
||||
batch = sources[i : i + batch_size]
|
||||
result = semantica.build_knowledge_base(batch)
|
||||
# Save and clear memory
|
||||
save_result(result)
|
||||
del result
|
||||
gc.collect()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Custom Plugins
|
||||
|
||||
Create custom plugins:
|
||||
### Custom Plugin
|
||||
|
||||
```python
|
||||
from semantica.core import Plugin
|
||||
|
||||
class CustomPlugin(Plugin):
|
||||
def process(self, data):
|
||||
# Your custom processing
|
||||
# Your custom processing logic
|
||||
return processed_data
|
||||
```
|
||||
|
||||
### Custom Extractors
|
||||
|
||||
Implement custom extractors:
|
||||
### Custom Extractor
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract import BaseExtractor
|
||||
|
||||
class DomainSpecificExtractor(BaseExtractor):
|
||||
def extract_entities(self, text):
|
||||
# Domain-specific extraction logic
|
||||
def extract(self, text):
|
||||
# Domain-specific entity extraction logic
|
||||
return entities
|
||||
```
|
||||
|
||||
## Internal APIs
|
||||
### Custom Ingestor
|
||||
|
||||
### Core APIs
|
||||
```python
|
||||
from semantica.ingest import BaseIngestor
|
||||
|
||||
- `Semantica.build_knowledge_base()` - Main entry point
|
||||
- `KGBuilder.build()` - Graph construction
|
||||
- `ConflictResolver.resolve()` - Conflict resolution
|
||||
- `EmbeddingGenerator.generate()` - Embedding generation
|
||||
|
||||
### Extension APIs
|
||||
|
||||
- Plugin registration
|
||||
- Custom extractor registration
|
||||
- Custom exporter registration
|
||||
- Event hooks
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why Modular Architecture?
|
||||
|
||||
- **Extensibility**: Easy to add new features
|
||||
- **Testability**: Components can be tested independently
|
||||
- **Maintainability**: Clear separation of concerns
|
||||
- **Flexibility**: Swap implementations easily
|
||||
|
||||
### Why Conflict Resolution?
|
||||
|
||||
- **Data Quality**: Ensures consistent knowledge
|
||||
- **Multi-Source**: Handles conflicting information
|
||||
- **Flexibility**: Multiple resolution strategies
|
||||
- **Transparency**: Track resolution decisions
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements:
|
||||
|
||||
- Distributed processing
|
||||
- Real-time streaming
|
||||
- Advanced reasoning
|
||||
- Multi-modal support expansion
|
||||
- Enhanced visualization
|
||||
|
||||
## Contributing to Core
|
||||
|
||||
Interested in contributing to Semantica's core? See our [Contributing Guide](https://github.com/Hawksight-AI/semantica/blob/main/CONTRIBUTING.md).
|
||||
class CustomIngestor(BaseIngestor):
|
||||
def ingest(self, source):
|
||||
# Load and return document dicts
|
||||
return documents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more information:
|
||||
- **[API Reference](reference/core.md) - Detailed API documentation
|
||||
- **[Learning More](learning-more.md)** - Additional resources
|
||||
- **[GitHub Repository](https://github.com/Hawksight-AI/semantica)** - Source code
|
||||
## Internal APIs
|
||||
|
||||
| API | Purpose |
|
||||
|-----|---------|
|
||||
| `Semantica.build_knowledge_base()` | Main orchestration entry point |
|
||||
| `GraphBuilder.build()` | Graph construction |
|
||||
| `ConflictResolver.resolve()` | Conflict resolution |
|
||||
| `EmbeddingGenerator.generate()` | Embedding generation |
|
||||
|
||||
Extension hooks: plugin registration, custom extractor registration, custom exporter registration, event hooks.
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
**Why modular architecture?** Each component is independently testable and swappable. You can use `NERExtractor` alone without pulling in graph storage or pipelines.
|
||||
|
||||
**Why built-in conflict resolution?** Multi-source data always has contradictions. Ignoring them produces garbage graphs. Explicit resolution strategies give you control over data quality.
|
||||
|
||||
**Why W3C PROV-O for provenance?** It's an industry standard with tooling support. Using a custom format would make lineage data non-portable.
|
||||
|
||||
**Why multiple reasoning engines?** Different problems need different reasoning: forward chaining for rule application, SPARQL for graph queries, abductive for hypothesis generation. No single engine fits all cases.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Architecture](architecture.md) — high-level three-layer overview
|
||||
- [Modules](modules.md) — every module with code examples
|
||||
- [API Reference](reference/core.md) — complete technical reference
|
||||
- [Contributing](contributing.md) — how to extend the framework
|
||||
|
||||
+152
-398
@@ -1,306 +1,134 @@
|
||||
# Examples
|
||||
|
||||
Real-world examples and use cases for Semantica.
|
||||
|
||||
!!! tip "Interactive Learning"
|
||||
For hands-on interactive tutorials, check out our [Cookbook](cookbook.md) with Jupyter notebooks covering everything from basics to advanced use cases.
|
||||
Code examples organized by complexity. For interactive notebooks, see the [Cookbook](cookbook.md).
|
||||
|
||||
---
|
||||
|
||||
## Example Gallery
|
||||
## Beginner
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
### Basic Knowledge Graph
|
||||
|
||||
- :material-school: **Getting Started**
|
||||
---
|
||||
Quick examples to get you up and running in 5 minutes.
|
||||
|
||||
[View Examples](#getting-started-5-min-examples)
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
from semantica.parse import DocumentParser
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
- :material-cogs: **Core Workflows**
|
||||
---
|
||||
Common workflows for building production-ready graphs.
|
||||
|
||||
[View Examples](#core-workflows-15-min-examples)
|
||||
|
||||
- :material-rocket: **Advanced Patterns**
|
||||
---
|
||||
Complex use cases and production deployments.
|
||||
|
||||
[View Examples](#advanced-patterns-30-min-examples)
|
||||
|
||||
- :material-factory: **Production Patterns**
|
||||
---
|
||||
Scalable deployment patterns for enterprise use.
|
||||
|
||||
[View Examples](#production-patterns)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Getting Started (5 min examples)
|
||||
|
||||
### Example 1: Basic Knowledge Graph
|
||||
|
||||
**Difficulty**: Beginner
|
||||
|
||||
Build a knowledge graph from a single document using Semantica's modular approach. This example demonstrates the complete workflow from document ingestion to graph construction.
|
||||
|
||||
**What it demonstrates:**
|
||||
- Document ingestion and parsing
|
||||
- Entity and relationship extraction
|
||||
- Knowledge graph construction
|
||||
|
||||
**For complete step-by-step examples, see:**
|
||||
- **[Your First Knowledge Graph Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Complete walkthrough
|
||||
- **Topics**: Ingestion, parsing, extraction, graph building
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 20-30 minutes
|
||||
- **Use Cases**: Learning the complete workflow
|
||||
|
||||
### Example 2: Entity Extraction
|
||||
|
||||
**Difficulty**: Beginner
|
||||
|
||||
Extract entities from text using Named Entity Recognition. This example shows how to identify and classify named entities in text.
|
||||
|
||||
**What it demonstrates:**
|
||||
- Named Entity Recognition (NER)
|
||||
- Entity type classification
|
||||
- Confidence scoring
|
||||
|
||||
**For complete examples, see:**
|
||||
- **[Entity Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/05_Entity_Extraction.ipynb)**: Learn entity extraction
|
||||
- **Topics**: NER methods, entity types, extraction techniques
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Understanding entity extraction
|
||||
print(f"{entity.text}: {entity.label}")
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
Apple Inc.: ORGANIZATION
|
||||
Steve Jobs: PERSON
|
||||
```
|
||||
|
||||
### Example 3: Multi-Source Integration
|
||||
|
||||
**Difficulty**: Beginner
|
||||
|
||||
Combine data from multiple sources into a unified knowledge graph. This example demonstrates integrating data from diverse sources.
|
||||
|
||||
**What it demonstrates:**
|
||||
- Multi-source data ingestion
|
||||
- Entity merging and resolution
|
||||
- Unified graph construction
|
||||
|
||||
**For complete examples, see:**
|
||||
- **[Multi-Source Data Integration Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb)**: Advanced integration patterns
|
||||
- **Topics**: Multi-source integration, entity resolution, conflict handling
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Building unified knowledge graphs from diverse sources
|
||||
|
||||
---
|
||||
|
||||
## Core Workflows (15 min examples)
|
||||
|
||||
### Example 4: Conflict Resolution
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
Resolve conflicts in data from multiple sources. This example shows how to identify and resolve conflicting information.
|
||||
|
||||
**What it demonstrates:**
|
||||
- Conflict detection
|
||||
- Conflict resolution strategies
|
||||
- Data quality assurance
|
||||
|
||||
**For complete examples, see:**
|
||||
- **[Multi-Source Data Integration Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb)**: Conflict resolution patterns
|
||||
- **Topics**: Conflict detection, resolution strategies, data quality
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Data integration, quality assurance
|
||||
ingestor = FileIngestor()
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
rel = RelationExtractor()
|
||||
|
||||
all_entities = []
|
||||
for source in ["source1.pdf", "source2.pdf"]:
|
||||
doc = ingestor.ingest_file(source)
|
||||
parsed = parser.parse_document(source)
|
||||
text = parsed.get("full_text", "")
|
||||
entities = ner.extract_entities(text)
|
||||
all_entities.extend(entities)
|
||||
sources = ingestor.ingest("data/sample.pdf")
|
||||
parsed = parser.parse(sources[0])
|
||||
|
||||
entities = ner.extract(parsed)
|
||||
relationships = rel.extract(parsed, entities=entities)
|
||||
|
||||
kg = GraphBuilder(merge_entities=True).build(
|
||||
entities=entities, relationships=relationships
|
||||
)
|
||||
print(f"{len(kg.nodes)} nodes, {len(kg.edges)} edges")
|
||||
```
|
||||
|
||||
### Entity Extraction from Text
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
|
||||
ner = NERExtractor()
|
||||
entities = ner.extract("Apple Inc. was founded by Steve Jobs in 1976.")
|
||||
|
||||
for entity in entities:
|
||||
print(f"{entity['text']}: {entity['type']}")
|
||||
# Apple Inc.: ORGANIZATION
|
||||
# Steve Jobs: PERSON
|
||||
# 1976: DATE
|
||||
```
|
||||
|
||||
### Custom NER Configuration
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
|
||||
ner = NERExtractor(
|
||||
method="llm",
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
confidence_threshold=0.8,
|
||||
temperature=0.0,
|
||||
)
|
||||
entities = ner.extract("Your document text here...")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Intermediate
|
||||
|
||||
### Multi-Source Integration
|
||||
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
from semantica.parse import DocumentParser
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
ingestor = FileIngestor()
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
rel = RelationExtractor()
|
||||
builder = GraphBuilder(merge_entities=True)
|
||||
|
||||
all_entities, all_rels = [], []
|
||||
|
||||
for path in ["source1.pdf", "source2.pdf", "source3.pdf"]:
|
||||
sources = ingestor.ingest(path)
|
||||
parsed = parser.parse(sources[0])
|
||||
all_entities.extend(ner.extract(parsed))
|
||||
all_rels.extend(rel.extract(parsed, entities=all_entities))
|
||||
|
||||
kg = builder.build(entities=all_entities, relationships=all_rels)
|
||||
print(f"Unified graph: {len(kg.nodes)} nodes, {len(kg.edges)} edges")
|
||||
```
|
||||
|
||||
### Conflict Detection and Resolution
|
||||
|
||||
```python
|
||||
from semantica.conflicts import ConflictDetector, ConflictResolver
|
||||
|
||||
# Detect and resolve conflicts
|
||||
detector = ConflictDetector()
|
||||
conflicts = detector.detect_conflicts(all_entities)
|
||||
|
||||
resolver = ConflictResolver(default_strategy="voting")
|
||||
resolved = resolver.resolve_conflicts(conflicts)
|
||||
|
||||
print(f"Detected {len(conflicts)} conflicts")
|
||||
print(f"Resolved {len(resolved)} conflicts")
|
||||
print(f"Detected {len(conflicts)} conflicts, resolved {len(resolved)}")
|
||||
```
|
||||
|
||||
### Example 5: Custom Entity Extraction Configuration
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
Use custom configuration for entity extraction with specific models and thresholds.
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
# Use LLM-based extraction with custom configuration
|
||||
ner = NERExtractor(
|
||||
method="llm",
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
confidence_threshold=0.8,
|
||||
temperature=0.0
|
||||
)
|
||||
|
||||
text = "Your document text here..."
|
||||
entities = ner.extract_entities(text)
|
||||
|
||||
# Build graph with custom merge settings
|
||||
builder = GraphBuilder(
|
||||
merge_entities=True,
|
||||
merge_threshold=0.9
|
||||
)
|
||||
kg = builder.build_graph(entities=entities, relationships=[])
|
||||
```
|
||||
|
||||
### Example 6: Incremental Graph Building
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
Build knowledge graph incrementally from multiple sources.
|
||||
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
from semantica.parse import DocumentParser
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder, GraphMerger
|
||||
|
||||
def build_kg_from_source(source_path):
|
||||
"""Helper function to build a knowledge graph from a single source."""
|
||||
ingestor = FileIngestor()
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
rel_extractor = RelationExtractor()
|
||||
|
||||
doc = ingestor.ingest_file(source_path)
|
||||
parsed = parser.parse_document(source_path)
|
||||
text = parsed.get("full_text", "")
|
||||
|
||||
entities = ner.extract_entities(text)
|
||||
relationships = rel_extractor.extract_relations(text, entities=entities)
|
||||
|
||||
builder = GraphBuilder()
|
||||
return builder.build_graph(entities=entities, relationships=relationships)
|
||||
|
||||
# Build graphs separately
|
||||
kg1 = build_kg_from_source("source1.pdf")
|
||||
kg2 = build_kg_from_source("source2.pdf")
|
||||
|
||||
# Merge into unified graph
|
||||
merger = GraphMerger()
|
||||
merged_kg = merger.merge([kg1, kg2])
|
||||
|
||||
print(f"Merged graph: {len(merged_kg.nodes)} nodes, {len(merged_kg.edges)} edges")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns (30+ min examples)
|
||||
|
||||
### Example 7: Graph Visualization
|
||||
|
||||
**Difficulty**: Beginner
|
||||
|
||||
Visualize your knowledge graph to understand entity relationships.
|
||||
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
from semantica.parse import DocumentParser
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
from semantica.visualization import KGVisualizer
|
||||
|
||||
# Build a small graph
|
||||
ingestor = FileIngestor()
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
rel_extractor = RelationExtractor()
|
||||
|
||||
doc = ingestor.ingest_file("semantica_intro.pdf")
|
||||
parsed = parser.parse_document("semantica_intro.pdf")
|
||||
text = parsed.get("full_text", "")
|
||||
|
||||
entities = ner.extract_entities(text)
|
||||
relationships = rel_extractor.extract_relations(text, entities=entities)
|
||||
|
||||
builder = GraphBuilder()
|
||||
kg = builder.build_graph(entities=entities, relationships=relationships)
|
||||
|
||||
# Visualize
|
||||
viz = KGVisualizer()
|
||||
viz.visualize_network(kg, output="html", file_path="semantica_knowledge_map.html")
|
||||
print("Visualization saved to semantica_knowledge_map.html")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns (30+ min examples)
|
||||
|
||||
### Example 8: Persistent Storage (Neo4j)
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
Store and query knowledge graphs in a persistent graph database.
|
||||
### Persistent Storage (Neo4j)
|
||||
|
||||
```python
|
||||
from semantica.graph_store import GraphStore
|
||||
|
||||
# Initialize with Neo4j
|
||||
store = GraphStore(
|
||||
backend="neo4j",
|
||||
uri="bolt://localhost:7687",
|
||||
user="neo4j",
|
||||
password="password"
|
||||
password="password",
|
||||
)
|
||||
store.connect()
|
||||
|
||||
# Create nodes and relationships
|
||||
apple = store.create_node(
|
||||
labels=["Company"],
|
||||
properties={"name": "Apple Inc."}
|
||||
)
|
||||
tim = store.create_node(
|
||||
labels=["Person"],
|
||||
properties={"name": "Tim Cook"}
|
||||
)
|
||||
apple = store.create_node(labels=["Company"], properties={"name": "Apple Inc."})
|
||||
tim = store.create_node(labels=["Person"], properties={"name": "Tim Cook"})
|
||||
store.create_relationship(
|
||||
start_node_id=tim["id"],
|
||||
end_node_id=apple["id"],
|
||||
rel_type="CEO_OF"
|
||||
rel_type="CEO_OF",
|
||||
)
|
||||
|
||||
store.close()
|
||||
```
|
||||
|
||||
### Example 9: FalkorDB for Real-Time Applications
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
Ultra-fast graph queries for LLM applications using FalkorDB.
|
||||
### FalkorDB (High-Speed Queries)
|
||||
|
||||
```python
|
||||
from semantica.graph_store import GraphStore
|
||||
@@ -309,105 +137,49 @@ store = GraphStore(
|
||||
backend="falkordb",
|
||||
host="localhost",
|
||||
port=6379,
|
||||
graph_name="knowledge_graph"
|
||||
graph_name="knowledge_graph",
|
||||
)
|
||||
store.connect()
|
||||
|
||||
# Fast queries
|
||||
results = store.execute_query("MATCH (n)-[r]->(m) WHERE n.name CONTAINS 'AI' RETURN n")
|
||||
results = store.execute_query(
|
||||
"MATCH (n)-[r]->(m) WHERE n.name CONTAINS 'AI' RETURN n"
|
||||
)
|
||||
store.close()
|
||||
```
|
||||
|
||||
### Example 10: GraphRAG (Knowledge-Powered Retrieval)
|
||||
---
|
||||
|
||||
**Difficulty**: Advanced
|
||||
## Advanced
|
||||
|
||||
Build a production-ready GraphRAG system with logical inference and hybrid retrieval.
|
||||
### GraphRAG with Reasoning
|
||||
|
||||
```python
|
||||
from semantica.context import AgentContext
|
||||
from semantica.reasoning import Reasoner
|
||||
|
||||
# 1. Initialize context with GraphRAG (Hybrid Retrieval)
|
||||
context = AgentContext(
|
||||
vector_store=vs,
|
||||
vector_store=vs,
|
||||
knowledge_graph=kg,
|
||||
graph_expansion=True,
|
||||
hybrid_alpha=0.7
|
||||
hybrid_alpha=0.7,
|
||||
)
|
||||
|
||||
# 2. Enrich Knowledge Graph using Logical Reasoning
|
||||
reasoner = Reasoner()
|
||||
|
||||
# Add a rule to categorize technology stack items
|
||||
reasoner.add_rule("IF Library(?x) AND Language(?y) THEN TechStackItem(?x)")
|
||||
inferred = reasoner.infer_facts(kg.get_all_triplets())
|
||||
|
||||
# Infer new facts from the existing graph
|
||||
all_facts = kg.get_all_triplets()
|
||||
inferred = reasoner.infer_facts(all_facts)
|
||||
for fact in inferred:
|
||||
kg.add_fact_from_string(fact)
|
||||
|
||||
# Add inferred knowledge back to the graph
|
||||
for fact_str in inferred:
|
||||
kg.add_fact_from_string(fact_str)
|
||||
|
||||
# 3. Retrieve context for a query (now with enriched knowledge)
|
||||
results = context.retrieve("What technologies are used in this project?")
|
||||
```
|
||||
|
||||
[**View Complete GraphRAG Tutorial**](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)
|
||||
|
||||
### Example 11: RAG vs. GraphRAG Comparison
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
Benchmark standard Vector RAG against Graph-enhanced retrieval.
|
||||
|
||||
[**View RAG vs. GraphRAG Comparison**](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)
|
||||
[Full GraphRAG tutorial](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb) · [RAG vs. GraphRAG comparison](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)
|
||||
|
||||
---
|
||||
|
||||
## Production Patterns
|
||||
## Production
|
||||
|
||||
### Example 12: Streaming Data Processing
|
||||
|
||||
**Difficulty**: Advanced
|
||||
|
||||
Process data streams in real-time.
|
||||
|
||||
```python
|
||||
from semantica.ingest import StreamIngestor
|
||||
from semantica.parse import DocumentParser
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
stream_ingestor = StreamIngestor(stream_uri="kafka://localhost:9092/topic")
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
rel_extractor = RelationExtractor()
|
||||
builder = GraphBuilder()
|
||||
|
||||
for batch in stream_ingestor.stream(batch_size=100):
|
||||
all_entities = []
|
||||
all_relationships = []
|
||||
|
||||
for item in batch:
|
||||
text = str(item) # Convert stream item to text
|
||||
entities = ner.extract_entities(text)
|
||||
relationships = rel_extractor.extract_relations(text, entities=entities)
|
||||
all_entities.extend(entities)
|
||||
all_relationships.extend(relationships)
|
||||
|
||||
# Build graph from batch
|
||||
kg = builder.build_graph(entities=all_entities, relationships=all_relationships)
|
||||
# Process results
|
||||
print(f"Processed batch: {len(kg.nodes)} nodes")
|
||||
```
|
||||
|
||||
### Example 13: Batch Processing Large Datasets
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
Process large datasets efficiently with batching.
|
||||
### Batch Processing (Large Datasets)
|
||||
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
@@ -416,75 +188,57 @@ from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
ingestor = FileIngestor()
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
rel_extractor = RelationExtractor()
|
||||
builder = GraphBuilder()
|
||||
parser = DocumentParser()
|
||||
ner = NERExtractor()
|
||||
rel = RelationExtractor()
|
||||
builder = GraphBuilder()
|
||||
|
||||
sources = [f"data/doc_{i}.pdf" for i in range(1000)]
|
||||
sources = [f"data/doc_{i}.pdf" for i in range(1000)]
|
||||
batch_size = 50
|
||||
|
||||
for i in range(0, len(sources), batch_size):
|
||||
batch = sources[i:i+batch_size]
|
||||
|
||||
all_entities = []
|
||||
all_relationships = []
|
||||
|
||||
for source in batch:
|
||||
doc = ingestor.ingest_file(source)
|
||||
parsed = parser.parse_document(source)
|
||||
text = parsed.get("full_text", "")
|
||||
|
||||
entities = ner.extract_entities(text)
|
||||
relationships = rel_extractor.extract_relations(text, entities=entities)
|
||||
|
||||
all_entities.extend(entities)
|
||||
all_relationships.extend(relationships)
|
||||
|
||||
# Build graph from batch
|
||||
kg = builder.build_graph(entities=all_entities, relationships=all_relationships)
|
||||
|
||||
# Save intermediate results
|
||||
print(f"Processed batch {i//batch_size + 1}: {len(kg.nodes)} nodes")
|
||||
batch = sources[i : i + batch_size]
|
||||
all_entities, all_rels = [], []
|
||||
|
||||
for path in batch:
|
||||
parsed = parser.parse(ingestor.ingest(path)[0])
|
||||
all_entities.extend(ner.extract(parsed))
|
||||
all_rels.extend(rel.extract(parsed, entities=all_entities))
|
||||
|
||||
kg = builder.build(entities=all_entities, relationships=all_rels)
|
||||
print(f"Batch {i // batch_size + 1}: {len(kg.nodes)} nodes")
|
||||
```
|
||||
|
||||
### Real-Time Streaming
|
||||
|
||||
```python
|
||||
from semantica.ingest import StreamIngestor
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
stream = StreamIngestor(stream_uri="kafka://localhost:9092/topic")
|
||||
ner = NERExtractor()
|
||||
rel = RelationExtractor()
|
||||
builder = GraphBuilder()
|
||||
|
||||
for batch in stream.stream(batch_size=100):
|
||||
all_entities, all_rels = [], []
|
||||
for item in batch:
|
||||
text = str(item)
|
||||
all_entities.extend(ner.extract(text))
|
||||
all_rels.extend(rel.extract(text, entities=all_entities))
|
||||
kg = builder.build(entities=all_entities, relationships=all_rels)
|
||||
print(f"Processed batch: {len(kg.nodes)} nodes")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## More Resources
|
||||
|
||||
- **[Quick Start Guide](quickstart.md)** - Step-by-step tutorial
|
||||
- **[API Reference](reference/core.md)** - Complete API documentation
|
||||
- **[Cookbook](cookbook.md)** - Interactive Jupyter notebooks
|
||||
- **[Use Cases](use-cases.md)** - Real-world applications
|
||||
|
||||
### 🍳 Recommended Cookbook Tutorials
|
||||
|
||||
- **[Welcome to Semantica](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: Comprehensive introduction to all modules
|
||||
- **Topics**: Framework overview, all modules, architecture, configuration
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: First-time users, understanding the framework
|
||||
|
||||
- **[Your First Knowledge Graph](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Build your first knowledge graph
|
||||
- **Topics**: Entity extraction, relationship extraction, graph construction, visualization
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 20-30 minutes
|
||||
- **Use Cases**: Learning the basics, quick start
|
||||
|
||||
- **[GraphRAG Complete](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)**: Production-ready GraphRAG system
|
||||
- **Topics**: GraphRAG, hybrid retrieval, vector search, graph traversal, LLM integration
|
||||
- **Difficulty**: Advanced
|
||||
- **Time**: 1-2 hours
|
||||
- **Use Cases**: Production RAG applications
|
||||
|
||||
- **[RAG vs. GraphRAG Comparison](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)**: Benchmark standard RAG vs GraphRAG
|
||||
- **Topics**: RAG, GraphRAG, benchmarking, visualization, reasoning gap
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 45-60 minutes
|
||||
- **Use Cases**: Understanding GraphRAG advantages, choosing the right approach
|
||||
|
||||
---
|
||||
|
||||
!!! info "Contribute"
|
||||
Have an example to share? [Contribute on GitHub](https://github.com/Hawksight-AI/semantica)
|
||||
- [Quickstart Tutorial](quickstart.md) — step-by-step first pipeline
|
||||
- [Cookbook](cookbook.md) — interactive Jupyter notebooks
|
||||
- [Use Cases](use-cases.md) — domain-specific examples
|
||||
- [API Reference](reference/core.md) — complete API documentation
|
||||
|
||||
!!! info "Have an example to share?"
|
||||
[Contribute on GitHub](https://github.com/Hawksight-AI/semantica)
|
||||
|
||||
+86
-85
@@ -1,148 +1,149 @@
|
||||
# Frequently Asked Questions
|
||||
# Frequently Asked Questions
|
||||
|
||||
**Common questions about Semantica and how to use it.**
|
||||
Common questions about Semantica. Use Ctrl+F to find what you need.
|
||||
|
||||
---
|
||||
|
||||
## General
|
||||
|
||||
### What is Semantica?
|
||||
Semantica is an open-source framework for building knowledge graphs from unstructured data. It transforms documents, web pages, and databases into structured, queryable knowledge.
|
||||
|
||||
### What can I do with Semantica?
|
||||
- **Build knowledge graphs** from documents and data
|
||||
- **Extract entities and relationships** automatically
|
||||
- **Power AI applications** with structured knowledge
|
||||
- **Create semantic search** and GraphRAG systems
|
||||
- **Integrate multiple data sources** into unified graphs
|
||||
Semantica is an open-source framework for building context graphs and decision intelligence layers for AI. It transforms unstructured data — documents, APIs, databases — into structured knowledge graphs with full provenance tracking, making AI systems explainable and auditable.
|
||||
|
||||
### What can I build with Semantica?
|
||||
|
||||
- Knowledge graphs from documents and multi-source data
|
||||
- GraphRAG systems with graph-grounded retrieval
|
||||
- AI agents with structured decision history and semantic memory
|
||||
- Compliance-ready pipelines with W3C PROV-O lineage
|
||||
|
||||
### What makes Semantica different from other frameworks?
|
||||
|
||||
Most frameworks stop at retrieval or generation. Semantica adds an **accountability layer**: every decision is recorded, every fact links to a source, and every reasoning step is explainable. It's designed for environments where you need to audit why an AI reached a conclusion.
|
||||
|
||||
### Is Semantica free?
|
||||
Yes! Semantica is open source under the MIT License.
|
||||
|
||||
### What makes Semantica different?
|
||||
- **Modular architecture** - Use only what you need
|
||||
- **Production-ready** - Built for scale and reliability
|
||||
- **Extensible** - Add custom models and components
|
||||
- **Open source** - Transparent and community-driven
|
||||
Yes — MIT licensed, no vendor lock-in. Some features require third-party API keys (e.g., OpenAI embeddings), but Semantica itself is free.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### How do I install Semantica?
|
||||
|
||||
```bash
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
See [Installation](installation.md) for virtual environment setup, optional extras, and troubleshooting.
|
||||
|
||||
### What Python version do I need?
|
||||
Python 3.8 or higher. Python 3.11+ is recommended.
|
||||
|
||||
Python 3.8 or higher. Python 3.11+ is recommended for best performance.
|
||||
|
||||
### What are the system requirements?
|
||||
|
||||
- Python 3.8+
|
||||
- 4GB+ RAM for basic use
|
||||
- Optional GPU for embeddings and ML models
|
||||
- 4 GB RAM minimum; 16 GB+ recommended for larger graphs
|
||||
- Optional GPU for embedding generation and ML inference
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### How do I start using Semantica?
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
### Where do I start?
|
||||
|
||||
# Extract entities
|
||||
ner = NERExtractor()
|
||||
entities = ner.extract("Apple Inc. was founded by Steve Jobs.")
|
||||
1. [Installation](installation.md) — get set up
|
||||
2. [Getting Started](getting-started.md) — core concepts and first example
|
||||
3. [Quickstart Tutorial](quickstart.md) — full step-by-step pipeline
|
||||
4. [Cookbook](cookbook.md) — interactive Jupyter notebooks
|
||||
|
||||
# Build knowledge graph
|
||||
kg = GraphBuilder().build({"entities": entities})
|
||||
```
|
||||
### What data sources does Semantica support?
|
||||
|
||||
### Where can I find examples?
|
||||
- **[Getting Started Guide](getting-started.md)** - Quick introduction
|
||||
- **[Cookbook](cookbook.md)** - Practical examples
|
||||
- **[GitHub Examples](https://github.com/Hawksight-AI/semantica/tree/main/examples)** - Code samples
|
||||
- **Files** — PDF, DOCX, HTML, JSON, CSV, Excel, PPTX, archives
|
||||
- **Web** — crawl with `WebIngestor`, RSS feeds
|
||||
- **Databases** — PostgreSQL, MySQL, Snowflake via `DBIngestor` / `SnowflakeIngestor`
|
||||
- **Streams** — Kafka, real-time ingestion
|
||||
- **Media** — image OCR, audio/video metadata
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### What data sources does Semantica support?
|
||||
- **Files**: PDF, DOCX, TXT, JSON, CSV
|
||||
- **Web**: Websites, RSS feeds, APIs
|
||||
- **Databases**: PostgreSQL, MySQL, Snowflake, MongoDB
|
||||
- **Streams**: Kafka, RabbitMQ, real-time data
|
||||
### Can I use my own models?
|
||||
|
||||
### Can I use custom models?
|
||||
Yes! Semantica supports custom:
|
||||
- **Entity extraction models**
|
||||
- **Embedding models**
|
||||
- **Language models**
|
||||
- **Custom processors**
|
||||
Yes. Semantica supports custom entity extraction models, embedding models, LLM providers (via LiteLLM — 100+ models), and custom pipeline processors.
|
||||
|
||||
### Does Semantica support GPUs?
|
||||
Yes, Semantica automatically uses GPUs when available for:
|
||||
- **Embedding generation**
|
||||
- **ML model inference**
|
||||
- **Vector operations**
|
||||
|
||||
Yes. When available, GPUs are used automatically for embedding generation, ML model inference, and vector operations. Install `semantica[gpu]` for CUDA support.
|
||||
|
||||
### How does Semantica handle large datasets?
|
||||
|
||||
- **Batching** — process documents in configurable chunks
|
||||
- **Parallel processing** — `PipelineBuilder` supports configurable worker counts
|
||||
- **Delta processing** — update graphs incrementally without full recompute
|
||||
- **Graph backends** — swap in-memory NetworkX for Neo4j, FalkorDB, or Apache AGE at scale
|
||||
|
||||
---
|
||||
|
||||
## Technical
|
||||
|
||||
### How does Semantica handle large datasets?
|
||||
- **Batching** - Process data in chunks
|
||||
- **Streaming** - Handle real-time data
|
||||
- **Parallel processing** - Use multiple cores
|
||||
- **Memory management** - Efficient resource usage
|
||||
### What graph databases are supported?
|
||||
|
||||
### Can I deploy Semantica in production?
|
||||
Yes! Semantica is production-ready with:
|
||||
- **Scalable architecture**
|
||||
- **Error handling**
|
||||
- **Monitoring support**
|
||||
- **Container deployment**
|
||||
Neo4j, FalkorDB, Apache AGE (PostgreSQL), Amazon Neptune, and in-memory NetworkX for development.
|
||||
|
||||
### How do I customize Semantica?
|
||||
- **Custom processors** - Add new extraction logic
|
||||
- **Custom models** - Use your own ML models
|
||||
- **Plugins** - Extend functionality
|
||||
- **Configuration** - Adjust behavior
|
||||
### What export formats are available?
|
||||
|
||||
RDF (Turtle, JSON-LD, N-Triples, XML), Apache Parquet, ArangoDB AQL, CSV, YAML, and OWL ontologies.
|
||||
|
||||
### Is Semantica production-ready?
|
||||
|
||||
Yes. v0.3.0 ships with 886+ passing tests, `PipelineValidator`, `FailureHandler` with exponential backoff, W3C PROV-O provenance, and change management with checksums. See [What's New](index.md#whats-new-in-v030) for details.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Installation issues
|
||||
- **Python version**: Ensure Python 3.8+
|
||||
- **Dependencies**: Install with `pip install -e .[dev]`
|
||||
- **Permissions**: Use virtual environments
|
||||
### Import error: `ModuleNotFoundError: No module named 'semantica'`
|
||||
|
||||
### Performance issues
|
||||
- **Memory**: Increase available RAM
|
||||
- **GPU**: Install CUDA for GPU acceleration
|
||||
- **Batching**: Use smaller chunk sizes
|
||||
Ensure you have the correct Python environment active, then:
|
||||
|
||||
### Common errors
|
||||
- **Import errors**: Check installation path
|
||||
- **Model loading**: Verify model availability
|
||||
- **Memory errors**: Reduce batch sizes
|
||||
```bash
|
||||
pip list | grep semantica
|
||||
pip install --upgrade semantica
|
||||
```
|
||||
|
||||
### Installation fails with dependency errors
|
||||
|
||||
```bash
|
||||
pip install --upgrade pip wheel
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
### Memory errors during processing
|
||||
|
||||
Reduce batch sizes, enable streaming ingestion, or switch to a persistent graph backend (Neo4j, FalkorDB).
|
||||
|
||||
### Slow embedding or inference
|
||||
|
||||
Install GPU support (`pip install semantica[gpu]`) and ensure CUDA is available on your system.
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
### Where can I get help?
|
||||
- **[GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)** - Report problems
|
||||
- **[Discussions](https://github.com/Hawksight-AI/semantica/discussions)** - Ask questions
|
||||
- **[Documentation](index.md)** - Browse guides and references
|
||||
|
||||
### How do I report bugs?
|
||||
1. **Search** existing issues first
|
||||
2. **Create** a new issue with details
|
||||
3. **Include** reproduction steps
|
||||
4. **Add** environment information
|
||||
- [Discord](https://discord.gg/sV34vps5hH) — community chat and support
|
||||
- [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues) — bug reports and feature requests
|
||||
- [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions) — questions and ideas
|
||||
|
||||
### Can I contribute?
|
||||
Yes! See the [Contributing Guide](contributing.md) for details on how to help improve Semantica.
|
||||
### How do I report a bug?
|
||||
|
||||
1. Search [existing issues](https://github.com/Hawksight-AI/semantica/issues) first
|
||||
2. Open a new issue with: description, reproduction steps, expected vs actual behavior, and your environment (Python version, OS, Semantica version)
|
||||
|
||||
### How do I contribute?
|
||||
|
||||
See the [Contributing Guide](contributing.md).
|
||||
|
||||
+53
-55
@@ -1,14 +1,18 @@
|
||||
# Getting Started
|
||||
|
||||
## Overview
|
||||
**Semantica** is the context and intelligence layer for AI — turning raw data into explainable, auditable knowledge graphs for high-stakes domains.
|
||||
|
||||
**Semantica** is a semantic intelligence layer that bridges the gap between raw data and trustworthy AI. It transforms unstructured data into explainable, auditable knowledge graphs perfect for high-stakes domains.
|
||||
!!! tip "Just here for code?"
|
||||
Jump straight to the [Quick Start](#quick-start) or explore the [Cookbook](cookbook.md) for interactive notebooks.
|
||||
|
||||
### What You Can Build
|
||||
- **GraphRAG Systems** - Enhanced retrieval with semantic reasoning
|
||||
- **AI Agents** - Trustworthy agents with explainable memory
|
||||
- **Knowledge Graphs** - Production-ready semantic databases
|
||||
- **Compliance-Ready AI** - Auditable systems with full provenance
|
||||
---
|
||||
|
||||
## What You Can Build
|
||||
|
||||
- **GraphRAG Systems** — enhanced retrieval with semantic graph reasoning
|
||||
- **AI Agents** — accountable agents with structured decision history and memory
|
||||
- **Knowledge Graphs** — production-ready semantic knowledge bases
|
||||
- **Compliance-Ready AI** — auditable systems with full W3C PROV-O provenance
|
||||
|
||||
---
|
||||
|
||||
@@ -18,17 +22,17 @@
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
Or with all features:
|
||||
With all optional dependencies:
|
||||
|
||||
```bash
|
||||
pip install semantica[all]
|
||||
```
|
||||
|
||||
Verify installation:
|
||||
Verify:
|
||||
|
||||
```python
|
||||
import semantica
|
||||
print(f"Semantica {semantica.__version__} installed!")
|
||||
print(semantica.__version__)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -36,66 +40,60 @@ print(f"Semantica {semantica.__version__} installed!")
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
from semantica.context import AgentContext, ContextGraph
|
||||
from semantica.vector_store import VectorStore
|
||||
|
||||
# Extract entities
|
||||
ner = NERExtractor(method="ml", model="en_core_web_sm")
|
||||
entities = ner.extract("Apple Inc. was founded by Steve Jobs in 1976.")
|
||||
context = AgentContext(
|
||||
vector_store=VectorStore(backend="faiss", dimension=768),
|
||||
vector_store=VectorStore(backend="inmemory"),
|
||||
knowledge_graph=ContextGraph(advanced_analytics=True),
|
||||
decision_tracking=True,
|
||||
)
|
||||
|
||||
# Build knowledge graph
|
||||
kg = GraphBuilder().build({"entities": entities, "relationships": []})
|
||||
print(f"Built KG with {len(kg.get('entities', []))} entities")
|
||||
# Store a memory
|
||||
context.store("GPT-4 outperforms GPT-3.5 on reasoning benchmarks by 40%")
|
||||
|
||||
# Record a decision
|
||||
decision_id = context.record_decision(
|
||||
category="model_selection",
|
||||
scenario="Choose LLM for production pipeline",
|
||||
reasoning="GPT-4 benchmark advantage justifies cost increase",
|
||||
outcome="selected_gpt4",
|
||||
confidence=0.91,
|
||||
)
|
||||
|
||||
# Find similar past decisions
|
||||
precedents = context.find_precedents("model selection", limit=5)
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
- Extracts entities (people, organizations, dates) from text
|
||||
- Builds a knowledge graph from extracted entities
|
||||
- Outputs the number of entities found
|
||||
|
||||
---
|
||||
|
||||
## Core Architecture
|
||||
|
||||
Semantica uses a **modular architecture** - use only what you need:
|
||||
Semantica uses a modular, layered architecture — import only what you need.
|
||||
|
||||
### 1️⃣ Input Layer - Data Ingestion
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
documents = FileIngestor().ingest_directory("docs/")
|
||||
```
|
||||
|
||||
### 2️⃣ Semantic Layer - Intelligence Engine
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
entities = NERExtractor().extract(text)
|
||||
relationships = RelationExtractor().extract(text, entities)
|
||||
```
|
||||
|
||||
### 3️⃣ Output Layer - Knowledge Assets
|
||||
```python
|
||||
from semantica.kg import GraphBuilder
|
||||
kg = GraphBuilder().build_graph(entities, relationships)
|
||||
```
|
||||
| Layer | Modules | Purpose |
|
||||
|-------|---------|---------|
|
||||
| **Input** | `ingest`, `parse`, `split`, `normalize` | Load and prepare data |
|
||||
| **Semantic** | `semantic_extract`, `kg`, `ontology`, `reasoning` | Extract meaning |
|
||||
| **Storage** | `embeddings`, `vector_store`, `graph_store` | Persist knowledge |
|
||||
| **Quality** | `deduplication`, `conflicts` | Validate and clean |
|
||||
| **Context** | `context`, `provenance`, `change_management` | Track decisions and lineage |
|
||||
| **Output** | `export`, `visualization`, `pipeline` | Deliver results |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### 🍳 Interactive Tutorials
|
||||
1. **[Welcome to Semantica](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)** - Complete framework overview
|
||||
2. **[Your First Knowledge Graph](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)** - Hands-on graph building
|
||||
3. **[GraphRAG Complete](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)** - Production-ready RAG
|
||||
|
||||
### 📚 Learn More
|
||||
- **[Core Concepts](concepts.md)** - Deep dive into knowledge graphs & ontologies
|
||||
- **[Cookbook](cookbook.md)** - 14 domain-specific tutorials
|
||||
- **[API Reference](reference/core.md)** - Complete technical documentation
|
||||
- [Core Concepts](concepts.md) — knowledge graphs, ontologies, reasoning explained
|
||||
- [Quickstart Tutorial](quickstart.md) — build a full pipeline step by step
|
||||
- [Cookbook](cookbook.md) — 14 domain-specific Jupyter notebook tutorials
|
||||
- [API Reference](reference/core.md) — complete module documentation
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
## Help
|
||||
|
||||
- **[💬 Discord Community](https://discord.gg/N7WmAuDH)** - Get help from the community
|
||||
- **[🐛 Issues](https://github.com/Hawksight-AI/semantica/issues)** - Report bugs or request features
|
||||
- **[📖 Documentation](https://semantica.readthedocs.io/)** - Full documentation site
|
||||
- [Discord Community](https://discord.gg/sV34vps5hH) — ask questions, share projects
|
||||
- [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues) — report bugs or request features
|
||||
- [FAQ](faq.md) — common questions answered
|
||||
|
||||
+7
-17
@@ -1,9 +1,9 @@
|
||||
# Glossary
|
||||
|
||||
**Comprehensive reference of terms and concepts used in Semantica and semantic intelligence.**
|
||||
Reference of terms and concepts used throughout Semantica.
|
||||
|
||||
!!! tip "Quick Reference"
|
||||
Looking for a specific term? Use your browser's search function (Ctrl+F) to find terms quickly.
|
||||
!!! tip "Finding a term"
|
||||
Use Ctrl+F to search this page.
|
||||
|
||||
---
|
||||
|
||||
@@ -216,17 +216,7 @@ W3C PROV-O compliant tracking of data lineage and source attribution.
|
||||
|
||||
## See Also
|
||||
|
||||
- **[Core Concepts](concepts.md)** - Deep dive into fundamental concepts
|
||||
- **[Getting Started](getting-started.md)** - Begin your journey with Semantica
|
||||
- **[Modules Guide](modules.md)** - Complete module overview
|
||||
- **[API Reference](reference/)** - Technical documentation
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Documentation**: [Getting Started](getting-started.md)
|
||||
- **Examples**: [Cookbook](cookbook.md)
|
||||
- **Community**: [Discord](community.md)
|
||||
- **Issues**: [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)
|
||||
- **Support**: [Contact Us](community.md)
|
||||
- [Core Concepts](concepts.md) — deeper explanation of key ideas
|
||||
- [Getting Started](getting-started.md) — first steps
|
||||
- [Modules Guide](modules.md) — every module explained
|
||||
- [API Reference](reference/) — technical reference
|
||||
|
||||
+191
-290
@@ -1,24 +1,23 @@
|
||||
<div align="center">
|
||||
<img src="assets/img/Semantica Logo.png" alt="Semantica Logo" width="450" height="auto">
|
||||
|
||||
<img src="assets/img/Semantica Logo.png" alt="Semantica Logo" width="420" height="auto">
|
||||
|
||||
<h1>🧠 Semantica</h1>
|
||||
|
||||
|
||||
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.8+-blue.svg" alt="Python 3.8+"></a>
|
||||
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
||||
<a href="https://badge.fury.io/py/semantica"><img src="https://img.shields.io/badge/pypi-v0.2.3-blue.svg" alt="PyPI version"></a>
|
||||
<a href="https://pypi.org/project/semantica/"><img src="https://img.shields.io/pypi/dm/semantica" alt="Monthly Downloads"></a>
|
||||
<a href="https://pypi.org/project/semantica/"><img src="https://img.shields.io/pypi/v/semantica.svg" alt="PyPI"></a>
|
||||
<a href="https://github.com/Hawksight-AI/semantica/releases/tag/v0.4.0"><img src="https://img.shields.io/badge/version-0.4.0-brightgreen.svg" alt="Version"></a>
|
||||
<a href="https://pepy.tech/project/semantica"><img src="https://static.pepy.tech/badge/semantica" alt="Total Downloads"></a>
|
||||
<a href="https://semantica.readthedocs.io/"><img src="https://img.shields.io/badge/docs-latest-brightgreen.svg" alt="Documentation"></a>
|
||||
<a href="https://discord.gg/N7WmAuDH"><img src="https://img.shields.io/badge/Discord-Join%20Us-7289da?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
|
||||
<p><strong>Open-Source Semantic Layer & Knowledge Engineering Framework</strong></p>
|
||||
|
||||
<p><strong>Transform Chaos into Intelligence. Build AI systems that are explainable, traceable, and trustworthy — not black boxes.</strong></p>
|
||||
|
||||
<p><em>The semantic intelligence layer that makes your AI agents auditable, explainable, and trustworthy. Perfect for high-stakes domains where mistakes have real consequences.</em></p>
|
||||
|
||||
<p>🆓 <strong>Open Source</strong> • 📜 <strong>MIT Licensed</strong> • 🚀 <strong>Production Ready</strong> • 🌍 <strong>Community Driven</strong></p>
|
||||
|
||||
<a href="https://github.com/Hawksight-AI/semantica/actions"><img src="https://github.com/Hawksight-AI/semantica/workflows/CI/badge.svg" alt="CI"></a>
|
||||
<a href="https://discord.gg/sV34vps5hH"><img src="https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://x.com/BuildSemantica"><img src="https://img.shields.io/badge/X-Follow-black?logo=x&logoColor=white" alt="X"></a>
|
||||
|
||||
<p><strong>A Framework for Building Context Graphs and Decision Intelligence Layers for AI</strong></p>
|
||||
|
||||
<p>⭐ Give us a Star • 🍴 Fork us • 💬 Join our Discord • 🐦 Follow on X</p>
|
||||
|
||||
<p><em>Transform Chaos into Intelligence. Build AI systems with context graphs, decision tracking, and advanced knowledge engineering that are explainable, traceable, and trustworthy — not black boxes.</em></p>
|
||||
|
||||
<p>
|
||||
<a href="getting-started/" class="md-button md-button--primary">Get Started</a>
|
||||
<a href="https://github.com/Hawksight-AI/semantica" class="md-button">View on GitHub</a>
|
||||
@@ -27,288 +26,266 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Why Semantica?
|
||||
## The Problem
|
||||
|
||||
**Semantica** bridges the **semantic gap** between text similarity and true meaning. It's the **semantic intelligence layer** that makes your AI agents auditable, explainable, and trustworthy.
|
||||
AI agents today are capable but not trustworthy:
|
||||
|
||||
Perfect for **high-stakes domains** where mistakes have real consequences.
|
||||
- **No memory structure** — agents store embeddings, not meaning. Retrieval is fuzzy; there's no way to ask *why* something was recalled.
|
||||
- **No decision trail** — agents make decisions continuously but record nothing. When something goes wrong, there's no history to debug or audit.
|
||||
- **No provenance** — outputs cannot be traced back to source facts. In regulated industries, this is a compliance blocker.
|
||||
- **No reasoning transparency** — black-box answers with no explanation of how a conclusion was reached.
|
||||
- **No conflict detection** — contradictory facts silently coexist in vector stores, producing unpredictable answers.
|
||||
|
||||
These aren't edge cases. They are the reason AI cannot be deployed in healthcare, finance, legal, and government without custom guardrails built from scratch.
|
||||
|
||||
---
|
||||
|
||||
### ⚡ Get Started in 30 Seconds
|
||||
## The Solution
|
||||
|
||||
Semantica is the **context and intelligence layer** you add to your AI stack:
|
||||
|
||||
- **Context Graphs** — structured graph of entities, relationships, and decisions your agent builds as it works. Queryable, traceable, persistent.
|
||||
- **Decision Intelligence** — every decision is a first-class object: recorded, linked causally, searchable by precedent, and analyzable for downstream impact.
|
||||
- **Provenance** — every fact links to its source. W3C PROV-O compliant. Full lineage from ingestion to inference.
|
||||
- **Reasoning engines** — forward chaining, Rete networks, deductive, abductive, and SPARQL reasoning. Explainable inference paths, not black-box answers.
|
||||
- **Deduplication & QA** — conflict detection, entity resolution, and validation built into the pipeline.
|
||||
|
||||
Works alongside LangChain, LlamaIndex, AutoGen, CrewAI, and any LLM provider — Semantica is not a replacement, it's the accountability layer on top.
|
||||
|
||||
---
|
||||
|
||||
### ⚡ Quick Installation
|
||||
|
||||
```bash
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
from semantica.context import AgentContext, ContextGraph
|
||||
from semantica.vector_store import VectorStore
|
||||
|
||||
# Extract entities and build knowledge graph
|
||||
ner = NERExtractor(method="ml", model="en_core_web_sm")
|
||||
entities = ner.extract("Apple Inc. was founded by Steve Jobs in 1976.")
|
||||
kg = GraphBuilder().build({"entities": entities, "relationships": []})
|
||||
context = AgentContext(
|
||||
vector_store=VectorStore(backend="faiss", dimension=768),
|
||||
knowledge_graph=ContextGraph(advanced_analytics=True),
|
||||
decision_tracking=True,
|
||||
)
|
||||
|
||||
print(f"Built KG with {len(kg.get('entities', []))} entities")
|
||||
# Store a memory
|
||||
context.store("GPT-4 outperforms GPT-3.5 on reasoning benchmarks by 40%")
|
||||
|
||||
# Record a decision
|
||||
decision_id = context.record_decision(
|
||||
category="model_selection",
|
||||
scenario="Choose LLM for production reasoning pipeline",
|
||||
reasoning="GPT-4 benchmark advantage justifies 3x cost increase",
|
||||
outcome="selected_gpt4",
|
||||
confidence=0.91,
|
||||
)
|
||||
|
||||
# Find similar past decisions and analyze downstream impact
|
||||
precedents = context.find_precedents("model selection reasoning", limit=5)
|
||||
influence = context.analyze_decision_influence(decision_id)
|
||||
```
|
||||
|
||||
**[📖 Full Quick Start](getting-started.md)** • **[🍳 Cookbook Examples](cookbook.md)** • **[💬 Join Discord](https://discord.gg/N7WmAuDH)** • **[⭐ Star Us](https://github.com/Hawksight-AI/semantica)**
|
||||
**[Full Quick Start](getting-started.md)** • **[Cookbook](cookbook.md)** • **[Join Discord](https://discord.gg/sV34vps5hH)**
|
||||
|
||||
---
|
||||
|
||||
## What's New in v0.3.0
|
||||
|
||||
> First stable release (`Production/Stable` on PyPI).
|
||||
|
||||
| Area | Highlights |
|
||||
|------|------------|
|
||||
| **Context Graphs** | Temporal validity windows, weighted BFS, cross-graph navigation with save/load persistence |
|
||||
| **Decision Intelligence** | Full lifecycle: record → trace → impact → precedent; `PolicyEngine` with versioned rules |
|
||||
| **KG Algorithms** | PageRank, betweenness, Louvain community detection, Node2Vec, link prediction |
|
||||
| **Semantic Extraction** | LLM extraction fixed (no silent drops), duplicate relation bug removed, `"llm_typed"` metadata corrected |
|
||||
| **Deduplication v2** | `blocking_v2`/`hybrid_v2` — 63.6% faster; semantic v2 — 6.98x faster |
|
||||
| **Delta Processing** | SPARQL-based incremental diff, `delta_mode` pipelines, snapshot versioning |
|
||||
| **Export** | RDF aliases (`"ttl"`, `"json-ld"`), ArangoDB AQL, Apache Parquet (Spark/BigQuery/Databricks) |
|
||||
| **Pipeline** | `FailureHandler` with LINEAR/EXPONENTIAL/FIXED backoff; `PipelineValidator` returning `ValidationResult` |
|
||||
| **Graph Backends** | Apache AGE (SQL injection fixed), AWS Neptune, FalkorDB, PgVector (HNSW/IVFFlat) |
|
||||
| **Tests** | 886+ passing, 0 failures — 335 context, ~430 KG, 70 semantic extraction, 85 real-world E2E |
|
||||
|
||||
---
|
||||
|
||||
## Core Value Proposition
|
||||
|
||||
| **Trustworthy** | **Explainable** | **Auditable** |
|
||||
|:------------------:|:------------------:|:-----------------:|
|
||||
|:---:|:---:|:---:|
|
||||
| Conflict detection & validation | Transparent reasoning paths | Complete provenance tracking |
|
||||
| Rule-based governance | Entity relationships & ontologies | W3C PROV-O compliant lineage |
|
||||
| Production-grade QA | Multi-hop graph reasoning | Source tracking & integrity verification |
|
||||
|
||||
---
|
||||
|
||||
## Key Features & Benefits
|
||||
## Features
|
||||
|
||||
### Not Just Another Agentic Framework
|
||||
### Context & Decision Intelligence
|
||||
- **Context Graphs** — structured, persistent graph of entities, relationships, and decisions
|
||||
- **Decision tracking** — `add_decision()`, `record_decision()` for full lifecycle management
|
||||
- **Causal chains** — `add_causal_relationship()`, `trace_decision_chain()`
|
||||
- **Precedent search** — hybrid similarity search over past decisions via `find_similar_decisions()`
|
||||
- **Influence analysis** — `analyze_decision_impact()`, `analyze_decision_influence()`
|
||||
- **Policy engine** — `check_decision_rules()` with versioned, automated compliance rules
|
||||
- **Agent memory** — `AgentMemory` with short/long-term storage and conversation history
|
||||
|
||||
**Semantica complements** LangChain, LlamaIndex, AutoGen, CrewAI, Google ADK, Agno, and other frameworks to enhance your agents with:
|
||||
### Knowledge Graphs
|
||||
- **Graph construction** — entities, relationships, properties, typed edges
|
||||
- **Algorithms** — PageRank, betweenness centrality, clustering coefficient, community detection
|
||||
- **Node embeddings** — Node2Vec via `NodeEmbedder`; cosine similarity via `SimilarityCalculator`
|
||||
- **Link prediction** — score potential edges via `LinkPredictor`
|
||||
- **Temporal graphs** — time-aware nodes and edges with validity windows
|
||||
- **Delta processing** — incremental updates without full recompute
|
||||
|
||||
| Feature | Benefit |
|
||||
|:--------|:--------|
|
||||
| **Auditable** | Complete provenance tracking with W3C PROV-O compliance |
|
||||
| **Explainable** | Transparent reasoning paths with entity relationships |
|
||||
| **Provenance-Aware** | End-to-end lineage from documents to responses |
|
||||
| **Validated** | Built-in conflict detection, deduplication, QA |
|
||||
| **Governed** | Rule-based validation and semantic consistency |
|
||||
| **Version Control** | Enterprise-grade change management with integrity verification |
|
||||
### Semantic Extraction
|
||||
- **NER** — named entity recognition, normalization, classification
|
||||
- **Relation extraction** — triplet generation via LLMs or rule-based methods, with `"llm_typed"` metadata
|
||||
- **Deduplication v1/v2** — Jaro-Winkler, `blocking_v2`, `hybrid_v2`, `semantic_v2`; `dedup_triplets()` for triples
|
||||
|
||||
### Perfect For High-Stakes Use Cases
|
||||
### Reasoning
|
||||
- **Forward chaining** — `Reasoner` with IF/THEN string rules and dict facts
|
||||
- **Rete network** — `ReteEngine` for high-throughput production rule matching
|
||||
- **Deductive / Abductive** — `DeductiveReasoner`, `AbductiveReasoner`
|
||||
- **SPARQL** — `SPARQLReasoner` for query-based inference over RDF graphs
|
||||
|
||||
| 🏥 **Healthcare** | 💰 **Finance** | ⚖️ **Legal** |
|
||||
|:-----------------:|:--------------:|:------------:|
|
||||
| Clinical decisions | Fraud detection | Evidence-backed research |
|
||||
| Drug interactions | Regulatory support | Contract analysis |
|
||||
| Patient safety | Risk assessment | Case law reasoning |
|
||||
### Provenance & Auditability
|
||||
- **Entity provenance** — `ProvenanceTracker.track_entity()`
|
||||
- **Algorithm provenance** — `AlgorithmTrackerWithProvenance`
|
||||
- **W3C PROV-O compliant** — lineage tracking across all modules
|
||||
- **Change management** — version control with checksums, audit trails, compliance support
|
||||
|
||||
| 🔒 **Cybersecurity** | 🏛️ **Government** | 🏭 **Infrastructure** | 🚗 **Autonomous** |
|
||||
|:-------------------:|:----------------:|:-------------------:|:-----------------:|
|
||||
| Threat attribution | Policy decisions | Power grids | Decision logs |
|
||||
| Incident response | Classified info | Transportation | Safety validation |
|
||||
### Vector Store
|
||||
- **Backends** — FAISS, Pinecone, Weaviate, Qdrant, Milvus, PgVector, in-memory
|
||||
- **Search modes** — semantic top-k, hybrid (vector + keyword), metadata-filtered
|
||||
|
||||
### Powers Your AI Stack
|
||||
### Data Ingestion
|
||||
- **Files** — PDF, DOCX, HTML, JSON, CSV, Excel, PPTX, archives
|
||||
- **Sources** — web crawl, SQL databases, Snowflake, feeds, email, repositories
|
||||
- **Docling** — advanced parsing with table and layout extraction
|
||||
- **Media** — image OCR, audio/video metadata
|
||||
|
||||
- **GraphRAG Systems** — Retrieval with graph reasoning and hybrid search
|
||||
- **AI Agents** — Trustworthy, accountable multi-agent systems with semantic memory
|
||||
- **Reasoning Models** — Explainable AI decisions with reasoning paths
|
||||
- **Enterprise AI** — Governed, auditable platforms that support compliance
|
||||
### Export
|
||||
- **RDF** — Turtle, JSON-LD, N-Triples, XML via `RDFExporter`
|
||||
- **Parquet** — `ParquetExporter` for Spark/BigQuery/Databricks pipelines
|
||||
- **ArangoDB AQL** — ready-to-run INSERT statements
|
||||
- **OWL ontologies** — Turtle or RDF/XML
|
||||
|
||||
### Integrations
|
||||
|
||||
- **Docling Support** — Document parsing with table extraction (PDF, DOCX, PPTX, XLSX)
|
||||
- **AWS Neptune** — Amazon Neptune graph database support with IAM authentication
|
||||
- **Custom Ontology Import** — Import existing ontologies (OWL, RDF, Turtle, JSON-LD)
|
||||
|
||||
> **Built for environments where every answer must be explainable and governed.**
|
||||
### Pipeline & Ontology
|
||||
- **Pipeline DSL** — `PipelineBuilder` with stage chaining, parallel workers, retry policies
|
||||
- **Ontology** — auto-generate OWL from KGs, import OWL/RDF/Turtle/JSON-LD, HermiT/Pellet validation
|
||||
|
||||
---
|
||||
|
||||
## 🚨 The Problem: The Semantic Gap
|
||||
## Modules
|
||||
|
||||
### Most AI systems fail in high-stakes domains because they operate on **text similarity**, not **meaning**.
|
||||
|
||||
### Understanding the Semantic Gap
|
||||
|
||||
The **semantic gap** is the fundamental disconnect between what AI systems can process (text patterns, vector similarities) and what high-stakes applications require (semantic understanding, meaning, context, and relationships).
|
||||
|
||||
**Traditional AI approaches:**
|
||||
- Rely on statistical patterns and text similarity
|
||||
- Cannot understand relationships between entities
|
||||
- Cannot reason about domain-specific rules
|
||||
- Cannot explain why decisions were made
|
||||
- Cannot trace back to original sources with confidence
|
||||
|
||||
**High-stakes AI requires:**
|
||||
- Semantic understanding of entities and their relationships
|
||||
- Domain knowledge encoded as formal rules (ontologies)
|
||||
- Explainable reasoning paths
|
||||
- Source-level provenance
|
||||
- Conflict detection and resolution
|
||||
|
||||
**Semantica bridges this gap** by providing a semantic intelligence layer that transforms unstructured data into validated, explainable, and auditable knowledge.
|
||||
|
||||
### What Organizations Have vs What They Need
|
||||
|
||||
| **Current State** | **Required for High-Stakes AI** |
|
||||
|:---------------------|:-----------------------------------|
|
||||
| PDFs, DOCX, emails, logs | Formal domain rules (ontologies) |
|
||||
| APIs, databases, streams | Structured and validated entities |
|
||||
| Conflicting facts and duplicates | Explicit semantic relationships |
|
||||
| Siloed systems with no lineage | **Explainable reasoning paths** |
|
||||
| | **Source-level provenance** |
|
||||
| | **Audit-ready compliance** |
|
||||
|
||||
### The Cost of Missing Semantics
|
||||
|
||||
- **Decisions cannot be explained** — No transparency in AI reasoning
|
||||
- **Errors cannot be traced** — No way to debug or improve
|
||||
- **Conflicts go undetected** — Contradictory information causes failures
|
||||
- **Compliance becomes impossible** — No audit trails for regulations
|
||||
|
||||
**Trustworthy AI requires semantic accountability.**
|
||||
| Module | What it provides |
|
||||
|--------|-----------------|
|
||||
| `semantica.context` | Context graphs, agent memory, decision tracking, causal analysis, precedent search, policy engine |
|
||||
| `semantica.kg` | KG construction, graph algorithms, centrality, community detection, embeddings, link prediction |
|
||||
| `semantica.semantic_extract` | NER, relation extraction, event extraction, coreference, triplet generation, LLM extraction |
|
||||
| `semantica.reasoning` | Forward chaining, Rete network, deductive, abductive, SPARQL reasoning, explanation generation |
|
||||
| `semantica.vector_store` | FAISS, Pinecone, Weaviate, Qdrant, Milvus, PgVector; hybrid & filtered search |
|
||||
| `semantica.export` | RDF, Parquet, ArangoDB AQL, CSV, YAML, OWL, graph formats |
|
||||
| `semantica.ingest` | Files, web crawl, feeds, databases, Snowflake, MCP, email, repositories |
|
||||
| `semantica.ontology` | Auto-generation, OWL/RDF export, import, validation, versioning |
|
||||
| `semantica.pipeline` | Pipeline DSL, parallel workers, validation, retry policies, failure handling |
|
||||
| `semantica.graph_store` | Neo4j, FalkorDB, Apache AGE, Amazon Neptune; Cypher queries |
|
||||
| `semantica.embeddings` | Sentence-Transformers, FastEmbed, OpenAI, BGE; similarity calculation |
|
||||
| `semantica.deduplication` | Entity deduplication, similarity scoring, merging, clustering |
|
||||
| `semantica.provenance` | W3C PROV-O lineage tracking, source attribution, audit trails |
|
||||
| `semantica.parse` | PDF, DOCX, PPTX, HTML, code, email, structured data, OCR |
|
||||
| `semantica.split` | Recursive, semantic, entity-aware, relation-aware, graph-based chunking |
|
||||
| `semantica.normalize` | Text, entities, dates, numbers, quantities, languages, encodings |
|
||||
| `semantica.conflicts` | Multi-source conflict detection (value, type, temporal, logical) with resolution |
|
||||
| `semantica.change_management` | Version storage, change tracking, checksums, audit trails |
|
||||
| `semantica.triplet_store` | Blazegraph, Jena, RDF4J; SPARQL queries and bulk loading |
|
||||
| `semantica.visualization` | Interactive/static KG, ontology, embedding, and temporal graph visualization |
|
||||
| `semantica.core` | Framework orchestration, configuration, plugin system |
|
||||
| `semantica.llms` | Groq, OpenAI, Novita AI, HuggingFace, LiteLLM integrations |
|
||||
|
||||
---
|
||||
|
||||
## 🆚 Semantica vs Traditional RAG
|
||||
## Built for High-Stakes Domains
|
||||
|
||||
| Feature | Traditional RAG | Semantica |
|
||||
|:--------|:----------------|:----------|
|
||||
| **Reasoning** | ❌ Black-box answers | ✅ Explainable reasoning paths |
|
||||
| **Provenance** | ❌ No provenance | ✅ W3C PROV-O compliant lineage tracking |
|
||||
| **Search** | ⚠️ Vector similarity only | ✅ Semantic + graph reasoning |
|
||||
| **Quality** | ❌ No conflict handling | ✅ Explicit contradiction detection |
|
||||
| **Safety** | ⚠️ Unsafe for high-stakes | ✅ Designed for governed environments |
|
||||
| **Compliance** | ❌ No audit trails | ✅ Complete audit trails with integrity verification |
|
||||
Where **every decision must be accountable** and **mistakes have real consequences**:
|
||||
|
||||
- **🏥 Healthcare & Life Sciences** — Clinical decision support, drug interactions, patient safety
|
||||
- **💰 Finance & Risk** — Fraud detection, SOX/GDPR/MiFID II compliance, risk assessment
|
||||
- **⚖️ Legal & Compliance** — Evidence-backed research, contract analysis, regulatory tracking
|
||||
- **🔒 Cybersecurity** — Threat attribution, incident response, security audit trails
|
||||
- **🏛️ Government & Defense** — Policy decisions, classified information handling, defense intelligence
|
||||
- **🏭 Critical Infrastructure** — Power grids, transportation safety, emergency response
|
||||
- **🚗 Autonomous Systems** — Self-driving, robotics safety, industrial automation
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Semantica Architecture
|
||||
|
||||
### 1️⃣ Input Layer — Governed Ingestion
|
||||
- 📄 **Multiple Formats** — PDFs, DOCX, HTML, JSON, CSV, Excel, PPTX
|
||||
- 🔧 **Docling Support** — Docling parser for table extraction
|
||||
- 💾 **Data Sources** — Databases, APIs, streams, archives, web content
|
||||
- 🎨 **Media Support** — Image parsing with OCR, audio/video metadata extraction
|
||||
- � **Single Pipeline** — Unified ingestion with metadata and source tracking
|
||||
|
||||
### 2️⃣ Semantic Layer — Trust & Reasoning Engine
|
||||
- 🔍 **Entity Extraction** — NER, normalization, classification
|
||||
- 🔗 **Relationship Discovery** — Triplet generation, semantic links
|
||||
- 📐 **Ontology Induction** — Automated domain rule generation
|
||||
- 🔄 **Deduplication** — Jaro-Winkler similarity, conflict resolution
|
||||
- ✅ **Quality Assurance** — Conflict detection, validation
|
||||
- 📊 **Provenance Tracking** — W3C PROV-O compliant lineage tracking across all modules
|
||||
- 🧠 **Reasoning Traces** — Explainable inference paths
|
||||
- 🔐 **Change Management** — Version control with audit trails, checksums, compliance support
|
||||
|
||||
### 3️⃣ Output Layer — Auditable Knowledge Assets
|
||||
- � **Knowledge Graphs** — Queryable, temporal, explainable
|
||||
- 📐 **OWL Ontologies** — HermiT/Pellet validated, custom ontology import support
|
||||
- 🔢 **Vector Embeddings** — FastEmbed by default
|
||||
- ☁️ **AWS Neptune** — Amazon Neptune graph database support
|
||||
- 🔍 **Provenance** — Every AI response links back to:
|
||||
- 📄 Source documents
|
||||
- 🏷️ Extracted entities & relations
|
||||
- 📐 Ontology rules applied
|
||||
- 🧠 Reasoning steps used
|
||||
|
||||
---
|
||||
|
||||
## 🏥 Built for High-Stakes Domains
|
||||
|
||||
Designed for domains where **mistakes have real consequences** and **every decision must be accountable**:
|
||||
|
||||
- **🏥 Healthcare & Life Sciences** — Clinical decision support, drug interaction analysis, medical literature reasoning, patient safety tracking
|
||||
- **💰 Finance & Risk** — Fraud detection, regulatory support (SOX, GDPR, MiFID II), credit risk assessment, algorithmic trading validation
|
||||
- **⚖️ Legal & Compliance** — Evidence-backed legal research, contract analysis, regulatory change tracking, case law reasoning
|
||||
- **🔒 Cybersecurity & Intelligence** — Threat attribution, incident response, security audit trails, intelligence analysis
|
||||
- **🏛️ Government & Defense** — Governed AI systems, policy decisions, classified information handling, defense intelligence
|
||||
- **🏭 Critical Infrastructure** — Power grid management, transportation safety, water treatment, emergency response
|
||||
- **🚗 Autonomous Systems** — Self-driving vehicles, drone navigation, robotics safety, industrial automation
|
||||
|
||||
---
|
||||
|
||||
## � Who Uses Semantica?
|
||||
|
||||
- **🤖 AI / ML Engineers** — Building explainable GraphRAG & agents
|
||||
- **⚙️ Data Engineers** — Creating governed semantic pipelines
|
||||
- **📊 Knowledge Engineers** — Managing ontologies & KGs at scale
|
||||
- **🏢 Enterprise Teams** — Requiring trustworthy AI infrastructure
|
||||
- **🛡️ Risk & Compliance Teams** — Needing audit-ready systems
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Choose Your Path
|
||||
## Choose Your Path
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-rocket-launch: **Quick Start**
|
||||
---
|
||||
Get up and running with Semantica in minutes. Learn the basics of ingestion and extraction.
|
||||
|
||||
Up and running in minutes.
|
||||
|
||||
[:arrow_right: Start Here](getting-started.md)
|
||||
|
||||
- :material-book-open-page-variant: **Core Concepts**
|
||||
---
|
||||
Deep dive into Knowledge Graphs, Ontologies, and Semantic Reasoning.
|
||||
|
||||
Knowledge graphs, ontologies, and semantic reasoning explained.
|
||||
|
||||
[:arrow_right: Learn Concepts](concepts.md)
|
||||
|
||||
- :material-code-braces: **API Reference**
|
||||
---
|
||||
Detailed technical documentation for all Semantica modules and classes.
|
||||
|
||||
Full technical documentation for every module and class.
|
||||
|
||||
[:arrow_right: View API](reference/core.md)
|
||||
|
||||
- :material-chef-hat: **Cookbook**
|
||||
---
|
||||
Interactive tutorials, real-world examples, and **14 domain-specific cookbooks**.
|
||||
|
||||
14 domain-specific cookbooks with real-world examples.
|
||||
|
||||
[:arrow_right: Explore Cookbook](cookbook.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
## Installation
|
||||
|
||||
!!! success "Now Available on PyPI!"
|
||||
Semantica is officially published on PyPI! Install it with a single command.
|
||||
Install with a single command.
|
||||
|
||||
=== "From PyPI (Recommended)"
|
||||
|
||||
Install Semantica directly from PyPI:
|
||||
=== "PyPI (Recommended)"
|
||||
|
||||
```bash
|
||||
# Install the core package
|
||||
pip install semantica
|
||||
|
||||
# Or install with all optional dependencies
|
||||
# With all optional dependencies
|
||||
pip install semantica[all]
|
||||
```
|
||||
|
||||
=== "From Source"
|
||||
|
||||
Install from the local source for the latest development version:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Hawksight-AI/semantica.git
|
||||
cd semantica
|
||||
|
||||
# Install in editable mode with core dependencies
|
||||
pip install -e .
|
||||
|
||||
# Or install with all optional dependencies
|
||||
pip install -e ".[all]"
|
||||
pip install -e . # core
|
||||
pip install -e ".[all]" # all extras
|
||||
```
|
||||
|
||||
=== "Development"
|
||||
|
||||
For contributors who want to modify the framework:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Hawksight-AI/semantica.git
|
||||
cd semantica
|
||||
|
||||
# Install in editable mode with dev dependencies
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
=== "Docker"
|
||||
|
||||
Run Semantica in a containerized environment:
|
||||
|
||||
```bash
|
||||
docker pull semantica/semantica:latest
|
||||
docker run -it semantica/semantica
|
||||
@@ -316,117 +293,41 @@ Designed for domains where **mistakes have real consequences** and **every decis
|
||||
|
||||
---
|
||||
|
||||
## 🚦 Quick Example
|
||||
|
||||
Semantica uses a modular architecture. You can use individual modules directly for maximum flexibility:
|
||||
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
from semantica.parse import DocumentParser
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
# 1. Ingest documents
|
||||
ingestor = FileIngestor()
|
||||
documents = ingestor.ingest_directory("documents/", recursive=True)
|
||||
|
||||
# 2. Parse documents
|
||||
parser = DocumentParser()
|
||||
parsed_docs = [parser.parse_document(doc) for doc in documents]
|
||||
|
||||
# 3. Extract entities and relationships
|
||||
ner = NERExtractor()
|
||||
rel_extractor = RelationExtractor()
|
||||
|
||||
entities = []
|
||||
relationships = []
|
||||
for doc in parsed_docs:
|
||||
text = doc.get("full_text", "")
|
||||
doc_entities = ner.extract_entities(text)
|
||||
doc_rels = rel_extractor.extract_relations(text, entities=doc_entities)
|
||||
entities.extend(doc_entities)
|
||||
relationships.extend(doc_rels)
|
||||
|
||||
# 4. Build knowledge graph
|
||||
builder = GraphBuilder(merge_entities=True)
|
||||
kg = builder.build_graph(entities=entities, relationships=relationships)
|
||||
|
||||
print(f"Created graph with {len(kg.nodes)} nodes and {len(kg.edges)} edges")
|
||||
```
|
||||
|
||||
!!! tip "Orchestration Option"
|
||||
For complex workflows, you can also use the `Semantica` class for orchestration. See the [Core Module](reference/core.md) documentation for details.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Why Semantica?
|
||||
## Why Semantica?
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- **🆓 Open Source**
|
||||
---
|
||||
MIT licensed. No vendor lock-in. Full transparency.
|
||||
MIT licensed. No vendor lock-in.
|
||||
|
||||
- **🚀 Production Ready**
|
||||
---
|
||||
Battle-tested with quality assurance, conflict resolution, and validation.
|
||||
Battle-tested with QA, conflict resolution, and validation built in.
|
||||
|
||||
- **🧩 Modular Architecture**
|
||||
- **🧩 Modular**
|
||||
---
|
||||
Use only what you need. Swap components easily.
|
||||
|
||||
- **🌍 Community Driven**
|
||||
---
|
||||
Built by developers, for developers. Active Discord community.
|
||||
Built by developers, for developers. Active Discord.
|
||||
|
||||
- **📚 Comprehensive**
|
||||
- **📚 End-to-End**
|
||||
---
|
||||
End-to-end solution from ingestion to reasoning. No duct-taping required.
|
||||
From ingestion to reasoning — no duct-taping required.
|
||||
|
||||
- **🔬 Research-Backed**
|
||||
---
|
||||
Based on latest research in knowledge graphs, ontologies, and semantic web.
|
||||
Grounded in knowledge graph, ontology, and semantic web research.
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Built For
|
||||
|
||||
- **Data Scientists**: Transform messy data into clean knowledge graphs
|
||||
- **Data Engineers**: Build scalable data pipelines with semantic enrichment
|
||||
- **AI Engineers**: Build GraphRAG, AI agents, and multi-agent systems
|
||||
- **Knowledge Engineers**: Generate and manage formal ontologies
|
||||
- **Ontologists**: Design and validate domain-specific ontologies and taxonomies
|
||||
- **Researchers**: Analyze scientific literature and build citation networks
|
||||
- **ML Engineers**: Create semantic features for machine learning models
|
||||
- **Enterprises**: Unify data silos into a semantic layer
|
||||
|
||||
---
|
||||
|
||||
## 📚 Learn More
|
||||
|
||||
- [Getting Started Guide](getting-started.md) - Your first knowledge graph in 5 minutes
|
||||
- [Core Concepts](concepts.md) - Deep dive into knowledge graphs and ontologies
|
||||
- [Cookbook](cookbook.md) - Real-world examples and **14 domain-specific cookbooks**
|
||||
- [API Reference](reference/core.md) - Complete technical documentation
|
||||
|
||||
### 🍳 Recommended Cookbook Tutorials
|
||||
|
||||
Get hands-on with interactive Jupyter notebooks:
|
||||
|
||||
- **[Welcome to Semantica](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: Comprehensive introduction to all Semantica modules
|
||||
- **Topics**: Framework overview, all modules, architecture
|
||||
- **Difficulty**: Beginner
|
||||
- **Use Cases**: First-time users, understanding the framework
|
||||
|
||||
- **[Your First Knowledge Graph](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Build your first knowledge graph from scratch
|
||||
- **Topics**: Entity extraction, relationship extraction, graph construction
|
||||
- **Difficulty**: Beginner
|
||||
- **Use Cases**: Learning the basics, quick start
|
||||
|
||||
- **[GraphRAG Complete](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)**: Production-ready Graph Retrieval Augmented Generation
|
||||
- **Topics**: GraphRAG, hybrid retrieval, vector search, graph traversal
|
||||
- **Difficulty**: Advanced
|
||||
- **Use Cases**: Building AI applications with knowledge graphs
|
||||
## Learn More
|
||||
|
||||
- [Getting Started](getting-started.md) — your first knowledge graph in 5 minutes
|
||||
- [Core Concepts](concepts.md) — knowledge graphs, ontologies, and semantic reasoning
|
||||
- [Cookbook](cookbook.md) — 14 domain-specific cookbooks with Jupyter notebooks
|
||||
- [API Reference](reference/core.md) — complete technical documentation
|
||||
|
||||
+112
-236
@@ -1,293 +1,169 @@
|
||||
# Installation
|
||||
|
||||
Get Semantica up and running in minutes.
|
||||
Get Semantica installed in under a minute.
|
||||
|
||||
!!! success "Now Available on PyPI!"
|
||||
Semantica is officially published on PyPI! Install it with a single command: `pip install semantica`
|
||||
!!! success "Available on PyPI"
|
||||
`pip install semantica` — that's it.
|
||||
|
||||
!!! note "System Requirements"
|
||||
Semantica requires Python 3.8 or higher. For best performance, we recommend Python 3.10+.
|
||||
!!! note "Requirements"
|
||||
Python 3.8 or higher. Python 3.11+ recommended.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing Semantica, ensure you have:
|
||||
|
||||
- **Python 3.8 or higher** - Check your version:
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
- **pip** - Python package installer (usually comes with Python)
|
||||
---
|
||||
|
||||
## Basic Installation
|
||||
|
||||
Install Semantica from PyPI:
|
||||
|
||||
```bash
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
This installs Semantica with all core dependencies.
|
||||
|
||||
### GitHub Workaround
|
||||
|
||||
If you encounter issues with the PyPI version, you can install directly from the main branch:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/Hawksight-AI/semantica.git@main
|
||||
```
|
||||
|
||||
!!! tip "Virtual Environment"
|
||||
We recommend installing Semantica in a virtual environment to avoid dependency conflicts. Use `python -m venv venv` to create one, then activate it before installing.
|
||||
|
||||
## Verify Installation
|
||||
|
||||
Verify that Semantica is installed correctly:
|
||||
|
||||
```bash
|
||||
python -c "from semantica.parse import DoclingParser; DoclingParser(); print('✓ Semantica ready')"
|
||||
```
|
||||
|
||||
!!! info "Windows PyTorch Note"
|
||||
If you encounter PyTorch DLL errors on Windows, ensure you have the [Microsoft Visual C++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) installed. This is a common environment-specific issue with PyTorch on Windows and not a bug in Semantica.
|
||||
|
||||
Expected output:
|
||||
```
|
||||
✓ Semantica ready
|
||||
```
|
||||
|
||||
You can also check the installation:
|
||||
|
||||
```bash
|
||||
pip show semantica
|
||||
```
|
||||
|
||||
## Development Installation
|
||||
|
||||
To install Semantica in development mode (for contributing):
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Hawksight-AI/semantica.git
|
||||
cd semantica
|
||||
|
||||
# Install in editable mode
|
||||
pip install -e .
|
||||
|
||||
# Or install with development dependencies
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
Semantica supports optional features that can be installed separately:
|
||||
|
||||
### GPU Support
|
||||
|
||||
For GPU-accelerated operations:
|
||||
|
||||
```bash
|
||||
pip install semantica[gpu]
|
||||
```
|
||||
|
||||
This includes:
|
||||
- PyTorch with CUDA support
|
||||
- FAISS GPU
|
||||
- CuPy
|
||||
|
||||
### Visualization
|
||||
|
||||
For enhanced visualization capabilities:
|
||||
|
||||
```bash
|
||||
pip install semantica[viz]
|
||||
```
|
||||
|
||||
Includes:
|
||||
- PyVis for interactive graphs
|
||||
- Graphviz for static diagrams
|
||||
- UMAP for dimensionality reduction
|
||||
|
||||
### LLM Providers
|
||||
|
||||
Install all LLM provider integrations:
|
||||
|
||||
```bash
|
||||
pip install semantica[llm-all]
|
||||
```
|
||||
|
||||
Or install specific providers:
|
||||
|
||||
```bash
|
||||
# OpenAI
|
||||
pip install semantica[llm-openai]
|
||||
|
||||
# Anthropic
|
||||
pip install semantica[llm-anthropic]
|
||||
|
||||
# Google Gemini
|
||||
pip install semantica[llm-gemini]
|
||||
|
||||
# Groq
|
||||
pip install semantica[llm-groq]
|
||||
|
||||
# Ollama
|
||||
pip install semantica[llm-ollama]
|
||||
```
|
||||
|
||||
### Cloud Integrations
|
||||
|
||||
For cloud storage and deployment:
|
||||
|
||||
```bash
|
||||
pip install semantica[cloud]
|
||||
```
|
||||
|
||||
Includes:
|
||||
- AWS S3 (boto3)
|
||||
- Azure Blob Storage
|
||||
- Google Cloud Storage
|
||||
- Kubernetes support
|
||||
|
||||
### All Optional Features
|
||||
|
||||
Install everything:
|
||||
With all optional dependencies:
|
||||
|
||||
```bash
|
||||
pip install semantica[all]
|
||||
```
|
||||
|
||||
## Virtual Environment (Recommended)
|
||||
### Verify
|
||||
|
||||
It's recommended to use a virtual environment:
|
||||
```bash
|
||||
python -c "import semantica; print(semantica.__version__)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Virtual Environment (Recommended)
|
||||
|
||||
=== "venv"
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate (Windows)
|
||||
venv\Scripts\activate
|
||||
|
||||
# Activate (Linux/Mac)
|
||||
source venv/bin/activate
|
||||
|
||||
# Install Semantica
|
||||
source venv/bin/activate # Linux / Mac
|
||||
venv\Scripts\activate # Windows
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
=== "conda"
|
||||
|
||||
```bash
|
||||
# Create conda environment
|
||||
conda create -n semantica python=3.11
|
||||
conda activate semantica
|
||||
|
||||
# Install Semantica
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
Install only what you need:
|
||||
|
||||
=== "GPU"
|
||||
|
||||
```bash
|
||||
pip install semantica[gpu]
|
||||
```
|
||||
Includes PyTorch with CUDA, FAISS GPU, CuPy.
|
||||
|
||||
=== "Visualization"
|
||||
|
||||
```bash
|
||||
pip install semantica[viz]
|
||||
```
|
||||
Includes PyVis, Graphviz, UMAP.
|
||||
|
||||
=== "LLM Providers"
|
||||
|
||||
```bash
|
||||
pip install semantica[llm-all] # all providers
|
||||
|
||||
pip install semantica[llm-openai] # OpenAI
|
||||
pip install semantica[llm-anthropic] # Anthropic
|
||||
pip install semantica[llm-gemini] # Google Gemini
|
||||
pip install semantica[llm-groq] # Groq
|
||||
pip install semantica[llm-ollama] # Ollama (local)
|
||||
```
|
||||
|
||||
=== "Cloud"
|
||||
|
||||
```bash
|
||||
pip install semantica[cloud]
|
||||
```
|
||||
Includes AWS S3, Azure Blob, Google Cloud Storage.
|
||||
|
||||
---
|
||||
|
||||
## Install from Source
|
||||
|
||||
For the latest development version or to contribute:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Hawksight-AI/semantica.git
|
||||
cd semantica
|
||||
|
||||
pip install -e . # core only
|
||||
pip install -e ".[all]" # all extras
|
||||
pip install -e ".[dev]" # dev tools (pytest, black, etc.)
|
||||
```
|
||||
|
||||
If you encounter issues with the PyPI release, install directly from the main branch:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/Hawksight-AI/semantica.git@main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
### ModuleNotFoundError
|
||||
|
||||
#### ModuleNotFoundError
|
||||
Check you have the right environment active:
|
||||
|
||||
**Error**: `ModuleNotFoundError: No module named 'semantica'`
|
||||
```bash
|
||||
pip list | grep semantica
|
||||
pip install --upgrade semantica
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
- Make sure you've activated the correct Python environment
|
||||
- Verify installation: `pip list | grep semantica`
|
||||
- Reinstall: `pip install --upgrade semantica`
|
||||
### Installation fails with dependency errors
|
||||
|
||||
#### Installation Fails
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install build wheel
|
||||
pip install semantica --no-deps # install without optional deps first
|
||||
```
|
||||
|
||||
**Error**: Installation fails with dependency errors
|
||||
### GPU dependencies fail
|
||||
|
||||
**Solutions**:
|
||||
- Upgrade pip: `pip install --upgrade pip`
|
||||
- Install build tools: `pip install build wheel`
|
||||
- Try installing without optional dependencies first: `pip install semantica --no-deps`
|
||||
Install CPU-only first, then add GPU support:
|
||||
|
||||
#### GPU Dependencies Fail
|
||||
```bash
|
||||
pip install semantica
|
||||
pip install semantica[gpu]
|
||||
```
|
||||
|
||||
**Error**: GPU dependencies fail to install
|
||||
### Permission denied
|
||||
|
||||
**Solutions**:
|
||||
- Install CPU-only version first: `pip install semantica`
|
||||
- Then add GPU support: `pip install semantica[gpu]`
|
||||
- Check CUDA compatibility for your system
|
||||
```bash
|
||||
pip install --user semantica # or use a virtual environment
|
||||
```
|
||||
|
||||
#### Permission Errors
|
||||
### Windows PyTorch DLL errors
|
||||
|
||||
**Error**: Permission denied during installation
|
||||
Install the [Microsoft Visual C++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe). This is a Windows system dependency, not a Semantica bug.
|
||||
|
||||
**Solutions**:
|
||||
- Use `--user` flag: `pip install --user semantica`
|
||||
- Use virtual environment (recommended)
|
||||
- On Linux/Mac, avoid using `sudo` with pip
|
||||
---
|
||||
|
||||
### System Requirements
|
||||
## System Requirements
|
||||
|
||||
| Component | Minimum | Recommended |
|
||||
|-----------|---------|-------------|
|
||||
| Python | 3.8 | 3.11+ |
|
||||
| RAM | Moderate | Ample for your dataset |
|
||||
| Disk Space | Sufficient for data | Generous storage |
|
||||
| OS | Windows/Linux/Mac | Linux/Mac |
|
||||
| OS | Windows / Linux / Mac | Linux / Mac |
|
||||
| RAM | 4 GB | 16 GB+ |
|
||||
| Storage | 2 GB | 20 GB+ (for models and data) |
|
||||
|
||||
## After Installation
|
||||
|
||||
Once Semantica is installed, verify your setup and get started:
|
||||
|
||||
### Verify Your Installation
|
||||
|
||||
Test that everything works correctly:
|
||||
|
||||
```bash
|
||||
python -c "import semantica; print(semantica.__version__)"
|
||||
```
|
||||
|
||||
**For detailed setup verification and first steps, see:**
|
||||
- **[Welcome to Semantica Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: Verify installation and explore all modules
|
||||
- **Topics**: Framework overview, installation verification, module exploration
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: First-time setup, understanding the framework
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that Semantica is installed:
|
||||
|
||||
1. **[Quick Start Guide](quickstart.md)** - Build your first knowledge graph in 5 minutes
|
||||
2. **[Getting Started Guide](getting-started.md)** - Learn the fundamentals
|
||||
3. **[Examples](examples.md)** - See real-world use cases
|
||||
4. **[Cookbook](cookbook.md)** - Interactive Jupyter notebook tutorials
|
||||
|
||||
### 🍳 Recommended First Cookbooks
|
||||
|
||||
Start with these interactive tutorials:
|
||||
|
||||
- **[Welcome to Semantica](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: Comprehensive introduction
|
||||
- **Topics**: Framework overview, all modules, architecture, configuration
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: First-time users, understanding the framework
|
||||
|
||||
- **[Your First Knowledge Graph](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Build your first graph
|
||||
- **Topics**: Entity extraction, relationship extraction, graph construction
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 20-30 minutes
|
||||
- **Use Cases**: Hands-on practice, quick start
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
- Check the [troubleshooting section](#troubleshooting) above
|
||||
- Review [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)
|
||||
- Ask questions in [GitHub Discussions](https://github.com/Hawksight-AI/semantica/discussions)
|
||||
|
||||
**For installation and setup help:**
|
||||
- **[Welcome to Semantica Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: Includes setup verification steps
|
||||
- **[Installation Troubleshooting Guide](getting-started.md#installation--setup)**: Additional troubleshooting tips
|
||||
- [Getting Started](getting-started.md) — build your first knowledge graph
|
||||
- [Quickstart Tutorial](quickstart.md) — full step-by-step pipeline
|
||||
- [Cookbook](cookbook.md) — interactive Jupyter notebook tutorials
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
# Agno Integration
|
||||
|
||||
Semantica's Agno integration (`semantica[agno]`) wires the full Semantica
|
||||
semantic intelligence stack into the [Agno](https://github.com/agno-agi/agno)
|
||||
agentic framework via five focused components.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Core integration
|
||||
pip install semantica[agno]
|
||||
|
||||
# With a graph store backend
|
||||
pip install semantica[agno,graph-neo4j]
|
||||
pip install semantica[agno,graph-falkordb]
|
||||
|
||||
# Full stack
|
||||
pip install semantica[agno,graph-neo4j,vectorstore-pgvector]
|
||||
```
|
||||
|
||||
## Components at a Glance
|
||||
|
||||
| Class | Agno Primitive | Semantica Backing |
|
||||
|---|---|---|
|
||||
| `AgnoContextStore` | `AgentMemory(db=…)` | `AgentContext` + `VectorStore` |
|
||||
| `AgnoKnowledgeGraph` | `Agent(knowledge=…)` | `ContextGraph` + KG pipeline |
|
||||
| `AgnoDecisionKit` | `Agent(tools=[…])` | `DecisionQuery`, `CausalChainAnalyzer`, `PolicyEngine` |
|
||||
| `AgnoKGToolkit` | `Agent(tools=[…])` | `NERExtractor`, `RelationExtractor`, `Reasoner` |
|
||||
| `AgnoSharedContext` | Team-level | Shared `ContextGraph` across agents |
|
||||
|
||||
---
|
||||
|
||||
## 1. AgnoContextStore
|
||||
|
||||
Replaces Agno's flat conversation storage with a hybrid **vector + context
|
||||
graph** memory store. Implements `agno.memory.db.base.MemoryDb`.
|
||||
|
||||
```python
|
||||
from agno.agent import Agent
|
||||
from agno.memory import AgentMemory
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
from semantica.context import ContextGraph
|
||||
from semantica.vector_store import VectorStore
|
||||
from integrations.agno import AgnoContextStore
|
||||
|
||||
store = AgnoContextStore(
|
||||
vector_store=VectorStore(backend="faiss"),
|
||||
knowledge_graph=ContextGraph(advanced_analytics=True),
|
||||
decision_tracking=True,
|
||||
graph_expansion=True,
|
||||
session_id="user_session_42",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
memory=AgentMemory(db=store),
|
||||
description="A financially aware assistant with persistent decision intelligence.",
|
||||
)
|
||||
|
||||
agent.print_response("Recommend a portfolio allocation for a risk-averse investor.")
|
||||
```
|
||||
|
||||
### Key behaviours
|
||||
|
||||
- `upsert_memory()` — stores text in `AgentContext` (vector index + graph node)
|
||||
- `read_memories()` — hybrid retrieval: vector similarity + optional graph hop expansion
|
||||
- `record_decision()` — records a structured decision with reasoning & outcome
|
||||
- `find_precedents()` — returns semantically similar historical decisions
|
||||
|
||||
---
|
||||
|
||||
## 2. AgnoKnowledgeGraph
|
||||
|
||||
Gives Agno agents a queryable `ContextGraph` instead of a flat document store.
|
||||
Ingested documents pass through the full Semantica extraction pipeline.
|
||||
|
||||
```python
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
from semantica.kg import GraphBuilder
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from integrations.agno import AgnoKnowledgeGraph
|
||||
|
||||
kg = AgnoKnowledgeGraph(
|
||||
graph_builder=GraphBuilder(),
|
||||
ner_extractor=NERExtractor(),
|
||||
relation_extractor=RelationExtractor(),
|
||||
)
|
||||
|
||||
# Ingest local files
|
||||
kg.load("regulatory_docs/", recursive=True)
|
||||
|
||||
# Ingest raw text
|
||||
kg.load(texts=["Basel IV capital requirements apply from January 2026."])
|
||||
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
knowledge=kg,
|
||||
search_knowledge=True,
|
||||
)
|
||||
```
|
||||
|
||||
### Ingestion pipeline
|
||||
|
||||
```
|
||||
parse → NER → relation extract → graph build → vector index
|
||||
```
|
||||
|
||||
### Search: multi-hop GraphRAG
|
||||
|
||||
```
|
||||
vector retrieval → entity lookup → graph hop expansion → context injection
|
||||
```
|
||||
|
||||
### Get entity subgraph
|
||||
|
||||
```python
|
||||
ctx = kg.get_graph_context("Basel IV")
|
||||
# Returns a text summary of the entity's immediate neighbourhood in the graph
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. AgnoDecisionKit
|
||||
|
||||
Exposes Semantica's decision intelligence as native Agno tools.
|
||||
|
||||
```python
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
from semantica.context import AgentContext
|
||||
from integrations.agno import AgnoDecisionKit
|
||||
|
||||
ctx = AgentContext(decision_tracking=True)
|
||||
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
tools=[AgnoDecisionKit(context=ctx)],
|
||||
show_tool_calls=True,
|
||||
)
|
||||
|
||||
agent.print_response("Should we approve this mortgage application?")
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
| Tool | Description | Key Parameters |
|
||||
|---|---|---|
|
||||
| `record_decision` | Record decision with reasoning and outcome | `category`, `scenario`, `reasoning`, `outcome`, `confidence`, `entities` |
|
||||
| `find_precedents` | Search for similar past decisions | `scenario`, `category`, `limit` |
|
||||
| `trace_causal_chain` | Trace causal chain of a decision | `decision_id`, `depth` |
|
||||
| `analyze_impact` | Assess downstream influence of a decision | `decision_id` |
|
||||
| `check_policy` | Validate decision against policy rules | `decision_data`, `policy_rules` |
|
||||
| `get_decision_summary` | Summarise decision history by category | `category`, `since`, `limit` |
|
||||
|
||||
### Example agent turn
|
||||
|
||||
```
|
||||
User: Should we approve this mortgage application?
|
||||
|
||||
Agent [tool: find_precedents] → 12 similar mortgage approvals found
|
||||
Agent [tool: check_policy] → complies with lending policy v2.3
|
||||
Agent [tool: record_decision] → recorded: loan_approval / approved / confidence=0.94
|
||||
Agent: Based on 12 historical precedents and full policy compliance, I recommend
|
||||
approval. Credit score 740, 22% down payment, DTI 31% — all within thresholds.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. AgnoKGToolkit
|
||||
|
||||
Lets agents actively build and query the context graph during reasoning.
|
||||
|
||||
```python
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
from integrations.agno import AgnoKGToolkit
|
||||
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
tools=[AgnoKGToolkit()],
|
||||
show_tool_calls=True,
|
||||
)
|
||||
|
||||
agent.print_response(
|
||||
"Extract entities and relationships from this article and store them in the knowledge graph."
|
||||
)
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `extract_entities` | Extract named entities from text |
|
||||
| `extract_relations` | Extract relationships between entities |
|
||||
| `add_to_graph` | Add entities / relations to the context graph |
|
||||
| `query_graph` | Query the graph (natural-language or Cypher) |
|
||||
| `find_related` | Find concepts related to a given entity |
|
||||
| `infer_facts` | Apply rules to infer new facts from the graph |
|
||||
| `export_subgraph` | Export a subgraph as RDF / JSON-LD |
|
||||
|
||||
---
|
||||
|
||||
## 5. AgnoSharedContext
|
||||
|
||||
A single `ContextGraph` shared across an Agno `Team`. Each agent gets a
|
||||
**role-scoped view** via `bind_agent()`.
|
||||
|
||||
```python
|
||||
from agno.agent import Agent
|
||||
from agno.team import Team
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
from semantica.context import ContextGraph
|
||||
from semantica.vector_store import VectorStore
|
||||
from integrations.agno import AgnoSharedContext, AgnoDecisionKit, AgnoKGToolkit
|
||||
|
||||
shared = AgnoSharedContext(
|
||||
vector_store=VectorStore(backend="faiss"),
|
||||
knowledge_graph=ContextGraph(advanced_analytics=True),
|
||||
decision_tracking=True,
|
||||
)
|
||||
|
||||
research_agent = Agent(
|
||||
name="Researcher",
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
memory=shared.bind_agent("researcher"),
|
||||
tools=[AgnoKGToolkit(context=shared)],
|
||||
)
|
||||
|
||||
decision_agent = Agent(
|
||||
name="Analyst",
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
memory=shared.bind_agent("analyst"),
|
||||
tools=[AgnoDecisionKit(context=shared)],
|
||||
)
|
||||
|
||||
team = Team(
|
||||
name="Research & Decision Team",
|
||||
agents=[research_agent, decision_agent],
|
||||
mode="coordinate",
|
||||
)
|
||||
|
||||
team.print_response(
|
||||
"Analyse the competitive landscape and recommend our product strategy."
|
||||
)
|
||||
```
|
||||
|
||||
### Shared memory pool
|
||||
|
||||
Memories written by one agent are immediately visible to all other agents in the
|
||||
team. Each agent's writes are tagged with their role so they can be filtered
|
||||
independently.
|
||||
|
||||
### Shared decisions
|
||||
|
||||
```python
|
||||
# Record a team-level decision
|
||||
decision_id = shared.record_decision(
|
||||
category="strategy",
|
||||
scenario="Expand to EU market",
|
||||
reasoning="Strong demand signals from Q1 survey",
|
||||
outcome="approved",
|
||||
confidence=0.87,
|
||||
agent_role="cfo",
|
||||
)
|
||||
|
||||
# Query precedents across all agents' history
|
||||
precedents = shared.find_precedents("market expansion")
|
||||
|
||||
# Get cross-agent analytics
|
||||
insights = shared.get_shared_insights()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Regulated Industry Agents (Finance, Healthcare, Legal)
|
||||
|
||||
Agents that log every decision with full provenance, reasoning chain, and policy
|
||||
compliance check for audit trails.
|
||||
|
||||
```python
|
||||
kit = AgnoDecisionKit(context=ctx)
|
||||
# Every agent turn: find_precedents → check_policy → record_decision
|
||||
```
|
||||
|
||||
### Long-Running Research Agents
|
||||
|
||||
Agents that accumulate a persistent `ContextGraph` over days or weeks, enabling
|
||||
multi-hop reasoning over a growing knowledge base.
|
||||
|
||||
```python
|
||||
kg = AgnoKnowledgeGraph(graph_builder=GraphBuilder(), ...)
|
||||
# Agents load new documents continuously; search benefits from the growing graph
|
||||
```
|
||||
|
||||
### Enterprise Multi-Agent Coordination
|
||||
|
||||
Teams using `AgnoSharedContext` to prevent contradictory decisions and share
|
||||
structured knowledge across specialist agents.
|
||||
|
||||
### GraphRAG Customer Support
|
||||
|
||||
Support agents that retrieve answers via graph traversal, providing more
|
||||
contextually grounded responses than flat vector search.
|
||||
|
||||
### Explainable AI Pipelines
|
||||
|
||||
Every agent step, entity reference, and causal chain is traceable back to a
|
||||
source document or prior decision.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
```python
|
||||
from integrations.agno import (
|
||||
AgnoContextStore, # MemoryDb implementation
|
||||
AgnoKnowledgeGraph, # AgentKnowledge implementation
|
||||
AgnoDecisionKit, # Decision intelligence Toolkit
|
||||
AgnoKGToolkit, # Knowledge graph Toolkit
|
||||
AgnoSharedContext, # Team-level shared context
|
||||
AGNO_AVAILABLE, # bool — True if agno is installed
|
||||
)
|
||||
```
|
||||
|
||||
All five classes are usable **without** `agno` installed — they carry the full
|
||||
Semantica API and degrade gracefully when passed to Agno constructors.
|
||||
@@ -13,7 +13,7 @@ Docling is integrated into Semantica's `parse` module via the `DoclingParser`. T
|
||||
|
||||
---
|
||||
|
||||
## 📖 Integration Documentation
|
||||
## Integration Documentation
|
||||
|
||||
The `DoclingParser` provides a high-level interface for document processing. It supports:
|
||||
|
||||
@@ -42,7 +42,7 @@ For more details, see the [Parse Reference](../reference/parse.md).
|
||||
|
||||
---
|
||||
|
||||
## 🧑🏽🍳 Integration Example
|
||||
## Integration Example
|
||||
|
||||
We provide a detailed cookbook and clear code examples to help you get started quickly.
|
||||
|
||||
@@ -81,7 +81,7 @@ See more in our [Code Examples](../CodeExamples.md).
|
||||
|
||||
---
|
||||
|
||||
## 💻 GitHub Source
|
||||
## GitHub Source
|
||||
|
||||
The integration is open-source and available on GitHub. You can explore the implementation, contribute improvements, or report issues.
|
||||
|
||||
@@ -89,7 +89,7 @@ The integration is open-source and available on GitHub. You can explore the impl
|
||||
|
||||
---
|
||||
|
||||
## 📦 PyPI & Installation
|
||||
## PyPI & Installation
|
||||
|
||||
Docling is an optional but highly recommended dependency for Semantica. You can install it along with Semantica or as a separate requirement.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Snowflake is integrated into Semantica's `ingest` module via the `SnowflakeInges
|
||||
|
||||
---
|
||||
|
||||
## 📖 Integration Documentation
|
||||
## Integration Documentation
|
||||
|
||||
The `SnowflakeIngestor` provides a high-level interface for Snowflake data ingestion. It supports:
|
||||
|
||||
@@ -42,7 +42,7 @@ For more details, see the [Ingest Reference](../reference/ingest.md).
|
||||
|
||||
---
|
||||
|
||||
## 🧑🏽🍳 Integration Example
|
||||
## Integration Example
|
||||
|
||||
We provide a detailed cookbook and clear code examples to help you get started quickly.
|
||||
|
||||
@@ -97,7 +97,7 @@ See more in our [Code Examples](../CodeExamples.md).
|
||||
|
||||
---
|
||||
|
||||
## 💻 GitHub Source
|
||||
## GitHub Source
|
||||
|
||||
The integration is open-source and available on GitHub. You can explore the implementation, contribute improvements, or report issues.
|
||||
|
||||
@@ -105,7 +105,7 @@ The integration is open-source and available on GitHub. You can explore the impl
|
||||
|
||||
---
|
||||
|
||||
## 📦 PyPI & Installation
|
||||
## PyPI & Installation
|
||||
|
||||
Snowflake connector is an optional dependency for Semantica. You can install it along with Semantica or as a separate requirement.
|
||||
|
||||
@@ -128,7 +128,7 @@ For full installation details, see the [Installation Guide](../installation.md).
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication Methods
|
||||
## Authentication Methods
|
||||
|
||||
Snowflake integration supports multiple authentication methods for different security requirements:
|
||||
|
||||
@@ -175,7 +175,7 @@ ingestor = SnowflakeIngestor(
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Advanced Features
|
||||
## Advanced Features
|
||||
|
||||
### Schema Introspection
|
||||
```python
|
||||
@@ -209,7 +209,7 @@ data = ingestor.ingest_query(
|
||||
|
||||
---
|
||||
|
||||
## 📊 Best Practices
|
||||
## Best Practices
|
||||
|
||||
### Use Environment Variables
|
||||
```python
|
||||
@@ -244,7 +244,7 @@ for page in range(total_pages):
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
```python
|
||||
@@ -272,7 +272,7 @@ ingestor = SnowflakeIngestor(
|
||||
|
||||
---
|
||||
|
||||
## 📚 See Also
|
||||
## See Also
|
||||
|
||||
- **[Ingest Module Reference](../reference/ingest.md)** - Complete ingestion documentation
|
||||
- **[Getting Started Guide](../getting-started.md)** - Quick start with Semantica
|
||||
|
||||
+73
-216
@@ -1,213 +1,104 @@
|
||||
# Learning More
|
||||
|
||||
Additional resources, tutorials, and advanced learning materials for Semantica.
|
||||
|
||||
!!! info "About This Guide"
|
||||
This guide provides structured learning paths, quick references, troubleshooting guides, and advanced topics to help you master Semantica.
|
||||
Structured learning paths, quick references, and performance guidance for going deeper with Semantica.
|
||||
|
||||
---
|
||||
|
||||
## Structured Learning Paths
|
||||
## Learning Paths
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-school: **Beginner Path**
|
||||
- :material-school: **Beginner** (1–2 hours)
|
||||
---
|
||||
Perfect for those new to Semantica and knowledge graphs.
|
||||
|
||||
New to Semantica and knowledge graphs.
|
||||
|
||||
|
||||
[Start Path](#beginner-path-1-2-hours)
|
||||
[Start here](#beginner-path)
|
||||
|
||||
- :material-compass: **Intermediate Path**
|
||||
- :material-compass: **Intermediate** (4–6 hours)
|
||||
---
|
||||
For users comfortable with basics who want to build production applications.
|
||||
|
||||
Comfortable with basics, building production applications.
|
||||
|
||||
|
||||
[Start Path](#intermediate-path-4-6-hours)
|
||||
[Start here](#intermediate-path)
|
||||
|
||||
- :material-rocket: **Advanced Path**
|
||||
- :material-rocket: **Advanced** (8+ hours)
|
||||
---
|
||||
For experienced users building enterprise applications.
|
||||
|
||||
Enterprise applications and customization.
|
||||
|
||||
|
||||
[Start Path](#advanced-path-8-hours)
|
||||
[Start here](#advanced-path)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
### Beginner Path (1-2 hours)
|
||||
### Beginner Path
|
||||
|
||||
1. **Installation & Setup** (15 min)
|
||||
- [Installation Guide](installation.md)
|
||||
- **[Welcome to Semantica Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: Comprehensive introduction
|
||||
- **Topics**: Framework overview, all modules, architecture, configuration
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: First-time users, understanding the framework
|
||||
|
||||
2. **Core Concepts** (30 min)
|
||||
- [Core Concepts](concepts.md)
|
||||
- [Getting Started Guide](getting-started.md)
|
||||
- **[Data Ingestion Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/02_Data_Ingestion.ipynb)**: Learn to ingest from multiple sources
|
||||
- **Topics**: File, web, feed, stream, database ingestion
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Loading data from various sources
|
||||
|
||||
3. **First Knowledge Graph** (30 min)
|
||||
- [Quickstart Tutorial](quickstart.md)
|
||||
- **[Your First Knowledge Graph Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Build your first knowledge graph
|
||||
- **Topics**: Entity extraction, relationship extraction, graph construction, visualization
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 20-30 minutes
|
||||
- **Use Cases**: Learning the basics, quick start
|
||||
|
||||
4. **Basic Operations** (30 min)
|
||||
- [Examples](examples.md)
|
||||
- **[Entity Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/05_Entity_Extraction.ipynb)**: Learn entity extraction
|
||||
- **Topics**: Named entity recognition, entity types, extraction methods
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Understanding entity extraction
|
||||
1. **Installation & Setup** — [Installation Guide](installation.md)
|
||||
2. **Core Concepts** — [Core Concepts](concepts.md) + [Getting Started](getting-started.md)
|
||||
3. **First Knowledge Graph** — [Quickstart Tutorial](quickstart.md)
|
||||
4. **Interactive Introduction** — [Welcome to Semantica notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)
|
||||
5. **Hands-On Practice** — [Your First Knowledge Graph notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)
|
||||
|
||||
---
|
||||
|
||||
### Intermediate Path (4-6 hours)
|
||||
### Intermediate Path
|
||||
|
||||
1. **Advanced Concepts** (1 hour)
|
||||
- [Modules Guide](modules.md)
|
||||
- **[Building Knowledge Graphs Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/07_Building_Knowledge_Graphs.ipynb)**: Advanced graph construction
|
||||
- **Topics**: Graph building, entity merging, conflict resolution, temporal graphs
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Production graph construction
|
||||
- **[Embeddings Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/09_Embeddings.ipynb)**: Learn embeddings
|
||||
- **Topics**: Embedding generation, similarity search, vector operations
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 20-30 minutes
|
||||
- **Use Cases**: Understanding embeddings, semantic search
|
||||
|
||||
2. **Use Cases** (1 hour)
|
||||
- [Use Cases Guide](use-cases.md)
|
||||
- **[GraphRAG Complete Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)**: Build production GraphRAG
|
||||
- **Topics**: GraphRAG, hybrid retrieval, graph traversal, LLM integration
|
||||
- **Difficulty**: Advanced
|
||||
- **Time**: 1-2 hours
|
||||
- **Use Cases**: Production GraphRAG systems
|
||||
|
||||
3. **Advanced Examples** (1 hour)
|
||||
- [Examples](examples.md)
|
||||
- **[Advanced Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/01_Advanced_Extraction.ipynb)**: Advanced extraction patterns
|
||||
- **Topics**: Custom entity types, domain-specific extraction, hybrid methods
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Domain-specific extraction
|
||||
|
||||
4. **Quality & Optimization** (1 hour)
|
||||
- [Quality Assurance](concepts.md#8-quality-assurance)
|
||||
- [Performance Optimization](#performance-optimization)
|
||||
- **[Multi-Source Data Integration Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb)**: Integrate multiple sources
|
||||
- **Topics**: Multi-source integration, entity resolution, conflict handling
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Building unified knowledge graphs
|
||||
1. **All Modules** — [Modules Guide](modules.md)
|
||||
2. **Advanced Graph Construction** — [Building Knowledge Graphs notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/07_Building_Knowledge_Graphs.ipynb)
|
||||
3. **Embeddings & Search** — [Embeddings notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/09_Embeddings.ipynb)
|
||||
4. **GraphRAG** — [GraphRAG Complete notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)
|
||||
5. **Multi-Source Integration** — [Multi-Source Data Integration notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb)
|
||||
6. **Use Case Examples** — [Use Cases](use-cases.md)
|
||||
|
||||
---
|
||||
|
||||
### Advanced Path (8+ hours)
|
||||
### Advanced Path
|
||||
|
||||
1. **Advanced Architecture** (2 hours)
|
||||
- [Architecture Guide](architecture.md)
|
||||
- **[Temporal Graphs Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/04_Temporal_Graphs.ipynb)**: Build temporal graphs
|
||||
- **Topics**: Time-stamped entities, temporal relationships, historical queries
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Time-aware knowledge graphs
|
||||
- **[Ontology Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/14_Ontology.ipynb)**: Generate ontologies
|
||||
- **Topics**: Ontology generation, OWL, schema design
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Formal knowledge representation
|
||||
|
||||
2. **Production Deployment** (2 hours)
|
||||
- [Security Best Practices](#security-best-practices)
|
||||
- **[GraphRAG Complete Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)**: Production GraphRAG
|
||||
- **Topics**: Production deployment, scalability, optimization
|
||||
- **Difficulty**: Advanced
|
||||
- **Time**: 1-2 hours
|
||||
- **Use Cases**: Production systems
|
||||
|
||||
3. **Customization** (2 hours)
|
||||
- **[Complete Visualization Suite Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/03_Complete_Visualization_Suite.ipynb)**: Advanced visualization
|
||||
- **Topics**: Custom layouts, filtering, styling, multiple graph types
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Production visualizations
|
||||
- **[Multi-Format Export Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/05_Multi_Format_Export.ipynb)**: Advanced export patterns
|
||||
- **Topics**: Batch export, custom formats, format conversion
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Production exports
|
||||
1. **Architecture Deep Dive** — [Architecture Guide](architecture.md)
|
||||
2. **Temporal Graphs** — [Temporal Graphs notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/04_Temporal_Graphs.ipynb)
|
||||
3. **Ontologies** — [Ontology notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/14_Ontology.ipynb)
|
||||
4. **Visualization** — [Complete Visualization Suite notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/03_Complete_Visualization_Suite.ipynb)
|
||||
5. **Export Pipelines** — [Multi-Format Export notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/05_Multi_Format_Export.ipynb)
|
||||
6. **Production GraphRAG** — [GraphRAG Complete notebook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
## Configuration Reference
|
||||
|
||||
### Common Operations
|
||||
|
||||
The typical workflow involves these steps:
|
||||
|
||||
1. **Ingest** documents using `` `FileIngestor` ``
|
||||
2. **Parse** documents using `` `DocumentParser` ``
|
||||
3. **Extract** entities and relationships using `` `NERExtractor` `` and `` `RelationExtractor` ``
|
||||
4. **Build** knowledge graph using `` `GraphBuilder` ``
|
||||
5. **Generate** embeddings using `` `TextEmbedder` ``
|
||||
|
||||
**For complete examples, see:**
|
||||
- **[Your First Knowledge Graph Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Complete workflow example
|
||||
- **[Welcome to Semantica Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: All modules overview
|
||||
|
||||
### Configuration Reference
|
||||
|
||||
| Setting | Environment Variable | Config File | Default |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| OpenAI API Key | `OPENAI_API_KEY` | `api_keys.openai` | `None` |
|
||||
| Embedding Provider | `SEMANTICA_EMBEDDING_PROVIDER` | `embedding.provider` | `"openai"` |
|
||||
| Graph Backend | `SEMANTICA_GRAPH_BACKEND` | `knowledge_graph.backend` | `"networkx"` |
|
||||
| Setting | Environment Variable | Default |
|
||||
|---------|---------------------|---------|
|
||||
| OpenAI API Key | `OPENAI_API_KEY` | `None` |
|
||||
| Embedding Provider | `SEMANTICA_EMBEDDING_PROVIDER` | `"openai"` |
|
||||
| Graph Backend | `SEMANTICA_GRAPH_BACKEND` | `"networkx"` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Guide
|
||||
## Troubleshooting
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-alert: **Import Errors**
|
||||
---
|
||||
`ModuleNotFoundError`
|
||||
|
||||
**Solution**: Verify installation (`pip list`) and Python version (3.8+).
|
||||
|
||||
Verify installation: `pip list | grep semantica`. Ensure Python 3.8+.
|
||||
|
||||
- :material-key: **API Key Errors**
|
||||
---
|
||||
`AuthenticationError`
|
||||
|
||||
**Solution**: Set `OPENAI_API_KEY` environment variable.
|
||||
|
||||
Set `OPENAI_API_KEY` (or the relevant provider key) as an environment variable.
|
||||
|
||||
- :material-memory: **Memory Errors**
|
||||
---
|
||||
`MemoryError`
|
||||
|
||||
**Solution**: Use batch processing and graph stores (Neo4j).
|
||||
`MemoryError` or OOM crashes
|
||||
|
||||
Reduce batch sizes or switch to a persistent graph backend (Neo4j, FalkorDB).
|
||||
|
||||
- :material-speedometer: **Slow Processing**
|
||||
---
|
||||
Long processing times
|
||||
|
||||
**Solution**: Enable parallel processing and GPU acceleration.
|
||||
Long runtimes on large datasets
|
||||
|
||||
Enable parallel processing (`PipelineBuilder` workers) and GPU acceleration.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -215,80 +106,46 @@ The typical workflow involves these steps:
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### 1. Batch Processing
|
||||
### Batch Processing
|
||||
|
||||
Process multiple documents together for better throughput. Use batch processing when working with large document collections.
|
||||
Process documents in batches rather than one at a time. Configure chunk sizes based on available RAM.
|
||||
|
||||
**For examples, see:**
|
||||
- **[Data Ingestion Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/02_Data_Ingestion.ipynb)**: Batch ingestion patterns
|
||||
- **[Multi-Source Data Integration Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb)**: Advanced integration
|
||||
### Parallel Execution
|
||||
|
||||
### 2. Parallel Execution
|
||||
`PipelineBuilder` supports configurable worker counts per stage for independent operations.
|
||||
|
||||
Use parallel processing for independent operations to improve performance on multi-core systems.
|
||||
### Backend Selection
|
||||
|
||||
### 3. Backend Selection
|
||||
| Operation | NetworkX | Neo4j / FalkorDB |
|
||||
|-----------|----------|------------------|
|
||||
| Graph construction | Fast | Moderate |
|
||||
| Query performance | Moderate | Fast |
|
||||
| Scalability | Low (in-memory) | High (persistent) |
|
||||
|
||||
| Operation | NetworkX | Neo4j |
|
||||
| :--- | :--- | :--- |
|
||||
| **Graph Construction** | ⚡⚡⚡ | ⚡⚡ |
|
||||
| **Query Performance** | ⚡⚡ | ⚡⚡⚡ |
|
||||
| **Scalability** | Low | High |
|
||||
Use NetworkX for development and smaller graphs; switch to a persistent backend for production at scale.
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### API Key Management
|
||||
**API keys**
|
||||
- Store in environment variables or a secrets manager
|
||||
- Never hardcode keys or commit them to version control
|
||||
- Rotate keys regularly
|
||||
|
||||
- **DO**: Use environment variables, rotate keys regularly.
|
||||
- **DON'T**: Hardcode keys, commit to version control.
|
||||
|
||||
### Data Privacy
|
||||
|
||||
- **DO**: Encrypt sensitive data, use local models.
|
||||
- **DON'T**: Send PII to external APIs without protection.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: What is Semantica?**
|
||||
A: A framework for building knowledge graphs and semantic applications.
|
||||
|
||||
**Q: Is Semantica free?**
|
||||
A: Yes, it is open source. Some features (e.g., OpenAI) require paid APIs.
|
||||
|
||||
**Q: Can I use Semantica in production?**
|
||||
A: Yes, it is designed for production with proper configuration.
|
||||
**Data privacy**
|
||||
- Use local embedding models for sensitive data
|
||||
- Avoid sending PII to external APIs without appropriate data handling agreements
|
||||
- Encrypt sensitive graph exports at rest
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue your learning journey:
|
||||
|
||||
- **[Cookbook](cookbook.md)** - Interactive Jupyter notebook tutorials
|
||||
- **[API Reference](reference/core.md)** - Complete API documentation
|
||||
- **[Use Cases](use-cases.md)** - Real-world applications
|
||||
- **[Examples](examples.md)** - Code examples and patterns
|
||||
|
||||
### 🍳 Recommended Next Cookbooks
|
||||
|
||||
- **[GraphRAG Complete](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)**: Production GraphRAG system
|
||||
- **Topics**: GraphRAG, hybrid retrieval, LLM integration
|
||||
- **Difficulty**: Advanced
|
||||
- **Time**: 1-2 hours
|
||||
- **Use Cases**: Production RAG applications
|
||||
|
||||
- **[RAG vs. GraphRAG Comparison](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)**: Understand the differences
|
||||
- **Topics**: RAG comparison, reasoning gap, inference engines
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 45-60 minutes
|
||||
- **Use Cases**: Choosing the right approach
|
||||
|
||||
---
|
||||
|
||||
!!! info "Contribute"
|
||||
Have questions? [Open an issue](https://github.com/Hawksight-AI/semantica/issues) or [start a discussion](https://github.com/Hawksight-AI/semantica/discussions)!
|
||||
- [Cookbook](cookbook.md) — interactive Jupyter notebook tutorials
|
||||
- [API Reference](reference/core.md) — complete technical documentation
|
||||
- [Use Cases](use-cases.md) — real-world domain examples
|
||||
- [FAQ](faq.md) — common questions
|
||||
|
||||
!!! info "Questions or feedback?"
|
||||
[Open an issue](https://github.com/Hawksight-AI/semantica/issues) or [start a discussion](https://github.com/Hawksight-AI/semantica/discussions).
|
||||
|
||||
+18
-93
@@ -1,9 +1,9 @@
|
||||
# Modules & Architecture
|
||||
# Modules
|
||||
|
||||
**Complete guide to Semantica's modular architecture and how to use each component.**
|
||||
Every Semantica module works independently — use only what you need.
|
||||
|
||||
!!! tip "Modular Design"
|
||||
Each Semantica module works independently. Use only what you need for your specific use case.
|
||||
!!! tip "Just need a quick reference?"
|
||||
Jump to the [Module Index](#module-index) at the bottom of this page.
|
||||
|
||||
---
|
||||
|
||||
@@ -82,13 +82,11 @@ web_ingestor = WebIngestor()
|
||||
pages = web_ingestor.ingest_urls(["https://example.com"])
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **File formats** - PDF, DOCX, TXT, JSON, CSV
|
||||
- **Web scraping** - Extract content from websites
|
||||
- **Database** - Connect to SQL and NoSQL databases
|
||||
- **Batch processing** - Handle large datasets efficiently
|
||||
|
||||
**Use Cases:**
|
||||
- Document processing pipelines
|
||||
- Web data extraction
|
||||
- Database integration
|
||||
@@ -106,13 +104,11 @@ text = parsed["full_text"]
|
||||
metadata = parsed["metadata"]
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Text extraction** - Extract clean text from documents
|
||||
- **Metadata parsing** - Extract titles, authors, dates
|
||||
- **Structure analysis** - Identify sections, headings
|
||||
- **OCR support** - Handle scanned documents
|
||||
|
||||
**Use Cases:**
|
||||
- PDF processing
|
||||
- Document analysis
|
||||
- Content extraction
|
||||
@@ -130,13 +126,11 @@ splitter = TextSplitter(method="semantic")
|
||||
chunks = splitter.split(text, chunk_size=1000, overlap=200)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Intelligent chunking** - Split text while preserving context
|
||||
- **Semantic splitting** - Break at natural boundaries
|
||||
- **Size control** - Manage chunk sizes for processing
|
||||
- **Overlap handling** - Maintain context between chunks
|
||||
|
||||
**Use Cases:**
|
||||
- Document preprocessing
|
||||
- Embedding preparation
|
||||
- RAG systems
|
||||
@@ -155,13 +149,11 @@ clean_text = normalizer.normalize_text(text)
|
||||
standardized_date = normalizer.normalize_date("Jan 1st, 2020")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Text cleaning** - Remove noise and artifacts
|
||||
- **Date standardization** - Convert to ISO format
|
||||
- **Name normalization** - Standardize person names
|
||||
- **Entity normalization** - Clean up company names
|
||||
|
||||
**Use Cases:**
|
||||
- Data preprocessing
|
||||
- Quality improvement
|
||||
- Standardization
|
||||
@@ -186,13 +178,11 @@ rel_extractor = RelationExtractor()
|
||||
relationships = rel_extractor.extract(text, entities)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Named Entity Recognition** - Find people, orgs, locations
|
||||
- **Relationship extraction** - Find connections between entities
|
||||
- **Custom entities** - Define your own entity types
|
||||
- **Confidence scoring** - Quality assessment for extractions
|
||||
|
||||
**Use Cases:**
|
||||
- Knowledge graph construction
|
||||
- Document analysis
|
||||
- Information extraction
|
||||
@@ -215,13 +205,11 @@ analyzer = GraphAnalyzer()
|
||||
stats = analyzer.analyze(kg)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Graph construction** - Build knowledge graphs from data
|
||||
- **Graph analysis** - Calculate metrics and statistics
|
||||
- **Graph querying** - Search and retrieve information
|
||||
- **Graph manipulation** - Merge, split, transform graphs
|
||||
|
||||
**Use Cases:**
|
||||
- Knowledge base creation
|
||||
- Graph analytics
|
||||
- Information retrieval
|
||||
@@ -244,13 +232,11 @@ ontology.add_relationship("works_for", "Person", "Organization")
|
||||
is_valid = ontology.validate_graph(kg)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Schema definition** - Define data structure
|
||||
- **Data validation** - Ensure data conforms to schema
|
||||
- **Inheritance** - Create hierarchical relationships
|
||||
- **Constraints** - Enforce data quality rules
|
||||
|
||||
**Use Cases:**
|
||||
- Data modeling
|
||||
- Quality assurance
|
||||
- Schema management
|
||||
@@ -268,13 +254,11 @@ engine = ReasoningEngine()
|
||||
inferences = engine.infer(kg, rules=["transitivity", "symmetry"])
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Logical inference** - Derive new facts from existing ones
|
||||
- **Pattern matching** - Find complex patterns in data
|
||||
- **Consistency checking** - Detect contradictions
|
||||
- **Decision support** - Automated reasoning
|
||||
|
||||
**Use Cases:**
|
||||
- Knowledge discovery
|
||||
- Decision making
|
||||
- Consistency checking
|
||||
@@ -295,13 +279,11 @@ embeddings = generator.generate(["text1", "text2"])
|
||||
similarity = generator.similarity(embeddings[0], embeddings[1])
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Text embeddings** - Convert text to vectors
|
||||
- **Similarity search** - Find similar content
|
||||
- **Clustering** - Group related items
|
||||
- **AI integration** - Provide context to LLMs
|
||||
|
||||
**Use Cases:**
|
||||
- Semantic search
|
||||
- Recommendation systems
|
||||
- Clustering
|
||||
@@ -320,13 +302,11 @@ store.add_vectors(embeddings, ids)
|
||||
results = store.search(query_vector, top_k=10)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Vector storage** - Efficient vector database
|
||||
- **Fast search** - Approximate nearest neighbor search
|
||||
- **Indexing** - Optimize for performance
|
||||
- **Batch operations** - Handle large datasets
|
||||
|
||||
**Use Cases:**
|
||||
- Semantic search
|
||||
- RAG systems
|
||||
- Recommendation engines
|
||||
@@ -346,13 +326,11 @@ store.add_edges(relationships)
|
||||
results = store.query("MATCH (n)-[r]->(m) RETURN n, r, m")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Graph persistence** - Store graphs in databases
|
||||
- **Graph queries** - Cypher and Gremlin support
|
||||
- **Graph algorithms** - Path finding, centrality
|
||||
- **Transactions** - ACID compliance
|
||||
|
||||
**Use Cases:**
|
||||
- Knowledge graph storage
|
||||
- Graph analytics
|
||||
- Network analysis
|
||||
@@ -371,13 +349,11 @@ store.add_triplets(subject, predicate, object)
|
||||
triplets = store.get_triplets(entity="Apple Inc.")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Triple storage** - Store (subject, predicate, object) triples
|
||||
- **Pattern matching** - Find specific patterns
|
||||
- **RDF support** - Semantic web standards
|
||||
- **Bulk operations** - Efficient batch processing
|
||||
|
||||
**Use Cases:**
|
||||
- Semantic web
|
||||
- Knowledge representation
|
||||
- Linked data
|
||||
@@ -397,13 +373,11 @@ resolver = EntityResolver()
|
||||
merged_entities = resolver.resolve(entities, strategy="semantic")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Duplicate detection** - Find similar entities
|
||||
- **Entity resolution** - Merge duplicate records
|
||||
- **Similarity scoring** - Quality assessment
|
||||
- **Record linkage** - Connect related records
|
||||
|
||||
**Use Cases:**
|
||||
- Data cleaning
|
||||
- Master data management
|
||||
- Record linkage
|
||||
@@ -422,13 +396,11 @@ conflicts = detector.detect_conflicts(kg)
|
||||
resolved = detector.resolve(conflicts, strategy="most_recent")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Conflict detection** - Find contradictory information
|
||||
- **Resolution strategies** - Automated conflict resolution
|
||||
- **Source reliability** - Trustworthiness assessment
|
||||
- **Temporal analysis** - Time-based conflict handling
|
||||
|
||||
**Use Cases:**
|
||||
- Data quality
|
||||
- Consistency checking
|
||||
- Trust management
|
||||
@@ -448,13 +420,11 @@ manager = ContextManager()
|
||||
context = manager.get_context(query, history)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Context tracking** - Maintain conversation context
|
||||
- **Memory management** - Store and retrieve context
|
||||
- **Relevance scoring** - Find relevant context
|
||||
- **Session management** - Handle multiple conversations
|
||||
|
||||
**Use Cases:**
|
||||
- AI agents
|
||||
- Chatbots
|
||||
- Conversational AI
|
||||
@@ -472,13 +442,11 @@ seed = SeedData()
|
||||
knowledge = seed.get_knowledge("technology", "companies")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Seed knowledge** - Foundation data for domains
|
||||
- **Knowledge bases** - Pre-built domain knowledge
|
||||
- **Quick start** - Bootstrap applications
|
||||
- **Domain models** - Industry-specific data
|
||||
|
||||
**Use Cases:**
|
||||
- Domain bootstrapping
|
||||
- Quick start data
|
||||
- Industry knowledge
|
||||
@@ -496,13 +464,11 @@ provider = LLMProvider(model="gpt-4")
|
||||
response = provider.generate(prompt, context=kg)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **LLM integration** - Connect to various LLM providers
|
||||
- **Prompt engineering** - Optimize prompts for results
|
||||
- **Context injection** - Provide knowledge graph context
|
||||
- **Response parsing** - Extract structured outputs
|
||||
|
||||
**Use Cases:**
|
||||
- AI generation
|
||||
- Question answering
|
||||
- Text completion
|
||||
@@ -522,13 +488,11 @@ exporter = GraphExporter()
|
||||
exporter.export(kg, format="json", filename="output.json")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Multiple formats** - JSON, CSV, RDF, GraphML
|
||||
- **Database export** - Export to various databases
|
||||
- **Streaming** - Handle large datasets
|
||||
- **Filtering** - Export specific data subsets
|
||||
|
||||
**Use Cases:**
|
||||
- Data sharing
|
||||
- System integration
|
||||
- Backup and restore
|
||||
@@ -546,13 +510,11 @@ visualizer = GraphVisualizer()
|
||||
visualizer.plot(kg, layout="force_directed")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Graph visualization** - Interactive graph plots
|
||||
- **Custom styling** - Tailored visual appearance
|
||||
- **Analytics charts** - Statistics and metrics
|
||||
- **Exploration tools** - Interactive data exploration
|
||||
|
||||
**Use Cases:**
|
||||
- Data exploration
|
||||
- Presentation
|
||||
- Analysis
|
||||
@@ -573,13 +535,11 @@ pipeline.add_step("build", GraphBuilder())
|
||||
result = pipeline.run("data/")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Workflow orchestration** - Coordinate multiple steps
|
||||
- **Parallel processing** - Run steps concurrently
|
||||
- **Progress tracking** - Monitor pipeline execution
|
||||
- **Error handling** - Robust error management
|
||||
|
||||
**Use Cases:**
|
||||
- Data processing
|
||||
- Workflow automation
|
||||
- Batch processing
|
||||
@@ -587,7 +547,7 @@ result = pipeline.run("data/")
|
||||
|
||||
---
|
||||
|
||||
## New Features & Modules
|
||||
## Additional Modules
|
||||
|
||||
### Change Management Module
|
||||
**Version control and audit trails**
|
||||
@@ -599,13 +559,11 @@ manager = TemporalVersionManager(storage_path="versions.db")
|
||||
snapshot = manager.create_snapshot(kg, "v1.0", "user@example.com", "Initial version")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Version control** - Track changes over time
|
||||
- **Audit trails** - Complete change history
|
||||
- **Data integrity** - SHA-256 checksums
|
||||
- **Change comparison** - Detailed diff analysis
|
||||
|
||||
**Use Cases:**
|
||||
- Knowledge graph versioning
|
||||
- Compliance tracking
|
||||
- Data governance
|
||||
@@ -623,13 +581,11 @@ manager = ProvenanceManager()
|
||||
manager.track_entity("entity_1", "document.pdf", "person")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **W3C PROV-O compliant** - Industry standard tracking
|
||||
- **Complete lineage** - End-to-end traceability
|
||||
- **Source attribution** - Track data origins
|
||||
- **Integrity verification** - Tamper detection
|
||||
|
||||
**Use Cases:**
|
||||
- Regulatory compliance
|
||||
- Data provenance
|
||||
- Audit trails
|
||||
@@ -648,13 +604,11 @@ semantica = Semantica(config=Config())
|
||||
result = semantica.process("data/")
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- **Framework orchestration** - Central coordination
|
||||
- **Configuration management** - Settings and preferences
|
||||
- **Lifecycle management** - Start/stop/restart
|
||||
- **Plugin system** - Extensible architecture
|
||||
|
||||
**Use Cases:**
|
||||
- Framework initialization
|
||||
- Configuration management
|
||||
- Plugin development
|
||||
@@ -662,46 +616,18 @@ result = semantica.process("data/")
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
## Common Module Chains
|
||||
|
||||
### Quick Start Example
|
||||
|
||||
```python
|
||||
# Complete pipeline example
|
||||
from semantica.ingest import FileIngestor
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
from semantica.pipeline import Pipeline
|
||||
|
||||
# Create pipeline
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_step("ingest", FileIngestor())
|
||||
pipeline.add_step("ner", NERExtractor())
|
||||
pipeline.add_step("relations", RelationExtractor())
|
||||
pipeline.add_step("build", GraphBuilder())
|
||||
|
||||
# Run pipeline
|
||||
kg = pipeline.run("documents/")
|
||||
print(f"Built graph with {len(kg['entities'])} entities")
|
||||
```
|
||||
|
||||
### Choose Your Modules
|
||||
|
||||
**For Document Processing:**
|
||||
- Ingest → Parse → Split → Semantic Extract → Knowledge Graph
|
||||
|
||||
**For Web Scraping:**
|
||||
- Ingest (Web) → Normalize → Semantic Extract → Graph Store
|
||||
|
||||
**For AI Agents:**
|
||||
- Context → LLM Providers → Reasoning → Export
|
||||
|
||||
**For Analytics:**
|
||||
- Knowledge Graph → Graph Store → Visualization → Export
|
||||
| Goal | Modules |
|
||||
|------|---------|
|
||||
| Document processing | Ingest → Parse → Split → Semantic Extract → KG |
|
||||
| Web scraping | Ingest (Web) → Normalize → Semantic Extract → Graph Store |
|
||||
| AI agents | Context → LLM Providers → Reasoning → Export |
|
||||
| Analytics | KG → Graph Store → Visualization → Export |
|
||||
|
||||
---
|
||||
|
||||
## Module Reference
|
||||
## Module Index
|
||||
|
||||
| Module | Purpose | Key Classes | Use Cases |
|
||||
|--------|---------|-------------|-----------|
|
||||
@@ -731,10 +657,9 @@ print(f"Built graph with {len(kg['entities'])} entities")
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
## More
|
||||
|
||||
- **Documentation**: [Getting Started](getting-started.md)
|
||||
- **Examples**: [Cookbook](cookbook.md)
|
||||
- **Community**: [Discord](community.md)
|
||||
- **Issues**: [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)
|
||||
- **Support**: [Contact Us](community.md)
|
||||
- [Getting Started](getting-started.md)
|
||||
- [Examples](examples.md)
|
||||
- [Cookbook](cookbook.md)
|
||||
- [API Reference](reference/core.md)
|
||||
|
||||
+96
-192
@@ -1,246 +1,150 @@
|
||||
# Quickstart
|
||||
|
||||
Get started with Semantica in 5 minutes. This guide will walk you through building your first knowledge graph.
|
||||
Build your first knowledge graph in 5 minutes.
|
||||
|
||||
!!! tip "Before You Start"
|
||||
Make sure you have Semantica installed. If not, follow the [Installation Guide](installation.md) first. This quickstart assumes basic Python knowledge.
|
||||
!!! tip "Prerequisites"
|
||||
Semantica installed (`pip install semantica`). If not, see the [Installation Guide](installation.md).
|
||||
|
||||
## Overview
|
||||
---
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Install] --> B[Initialize]
|
||||
B --> C[Load Data]
|
||||
C --> D[Extract]
|
||||
D --> E[Build Graph]
|
||||
E --> F[Visualize]
|
||||
|
||||
style A fill:#e3f2fd
|
||||
style F fill:#c8e6c9
|
||||
A[Ingest] --> B[Parse]
|
||||
B --> C[Extract]
|
||||
C --> D[Build Graph]
|
||||
D --> E[Visualize / Export]
|
||||
```
|
||||
|
||||
## Step 1: Installation
|
||||
---
|
||||
|
||||
If you haven't installed Semantica yet:
|
||||
## Step 1 — Ingest
|
||||
|
||||
```bash
|
||||
pip install semantica
|
||||
```
|
||||
Load documents from files, directories, or the web.
|
||||
|
||||
See the [Installation Guide](installation.md) for detailed instructions.
|
||||
|
||||
!!! note "Installation Options"
|
||||
For production use, consider installing with optional dependencies for better performance: `pip install semantica[all]`. See the [Installation Guide](installation.md) for all options.
|
||||
|
||||
## Step 2: Your First Knowledge Graph
|
||||
|
||||
Building a knowledge graph involves these key steps:
|
||||
|
||||
1. **Ingest** your documents using `FileIngestor`
|
||||
2. **Parse** documents to extract text using `DocumentParser` or `DoclingParser` (for enhanced layout support)
|
||||
3. **Extract** entities and relationships using `NERExtractor` and `RelationExtractor`
|
||||
4. **Build** the graph using `GraphBuilder`
|
||||
5. **Generate** embeddings (optional) using `TextEmbedder`
|
||||
|
||||
**Quick Example:**
|
||||
```python
|
||||
from semantica.ingest import FileIngestor
|
||||
from semantica.parse import DocumentParser, DoclingParser
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
# 1. Ingest document
|
||||
ingestor = FileIngestor()
|
||||
sources = ingestor.ingest("data/sample.pdf")
|
||||
|
||||
# 2. Parse (choose your parser)
|
||||
# Option A: Standard parser
|
||||
parser = DocumentParser()
|
||||
parsed_content = parser.parse(sources[0])
|
||||
|
||||
# Option B: Enhanced Docling parser (recommended for complex tables)
|
||||
# docling_parser = DoclingParser()
|
||||
# parsed_content = docling_parser.parse(sources[0])
|
||||
|
||||
# 3. Extract entities and relations
|
||||
ner = NERExtractor()
|
||||
entities = ner.extract(parsed_content)
|
||||
|
||||
relations = RelationExtractor()
|
||||
relationships = relations.extract(parsed_content, entities=entities)
|
||||
|
||||
# 4. Build graph
|
||||
builder = GraphBuilder()
|
||||
graph = builder.build(entities=entities, relationships=relationships)
|
||||
|
||||
print(f"Built knowledge graph with {len(graph.nodes)} nodes and {len(graph.edges)} edges")
|
||||
```
|
||||
|
||||
**For complete step-by-step examples with detailed explanations, see:**
|
||||
- **[Your First Knowledge Graph Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Full tutorial with detailed explanations and expected outputs
|
||||
- **Topics**: Entity extraction, relationship extraction, graph construction, visualization
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 20-30 minutes
|
||||
- **Use Cases**: Learning the basics, quick start
|
||||
Supported formats: PDF, DOCX, HTML, JSON, CSV, Excel, PPTX, archives. For web content, use `WebIngestor`.
|
||||
|
||||
## Step 3: Extract Entities and Relationships
|
||||
---
|
||||
|
||||
The semantic extraction step identifies named entities (people, organizations, locations) and relationships between them from your text.
|
||||
## Step 2 — Parse
|
||||
|
||||
**What gets extracted:**
|
||||
- **Entities**: People, organizations, locations, dates, and other named entities
|
||||
- **Relationships**: Connections between entities (e.g., `founded_by`, `located_in`, `has_ceo`)
|
||||
Extract structured text from raw documents.
|
||||
|
||||
**For detailed examples and different extraction methods, see:**
|
||||
- **[Entity Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/05_Entity_Extraction.ipynb)**: Learn different NER methods and configurations
|
||||
- **Topics**: Named entity recognition, entity types, confidence scores
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Understanding entity extraction options
|
||||
```python
|
||||
from semantica.parse import DocumentParser
|
||||
|
||||
- **[Relation Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/06_Relation_Extraction.ipynb)**: Learn to extract relationships between entities
|
||||
- **Topics**: Relationship extraction, dependency parsing, semantic role labeling
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Building rich knowledge graphs with relationships
|
||||
parser = DocumentParser()
|
||||
parsed = parser.parse(sources[0])
|
||||
```
|
||||
|
||||
## Step 4: Build Knowledge Graph from Multiple Sources
|
||||
For complex layouts (tables, columns): use `DoclingParser` instead — it handles PDF tables and structured DOCX/PPTX better.
|
||||
|
||||
You can combine data from multiple sources (files, web, databases) to build a unified knowledge graph. The process involves:
|
||||
---
|
||||
|
||||
1. **Ingest** from multiple sources using different ingestors
|
||||
2. **Parse** all documents to extract text
|
||||
3. **Extract** entities and relationships from each source
|
||||
4. **Build** a unified graph with entity merging enabled
|
||||
## Step 3 — Extract Entities and Relationships
|
||||
|
||||
**For complete examples with multiple sources, see:**
|
||||
- **[Data Ingestion Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/02_Data_Ingestion.ipynb)**: Learn to ingest from files, web, feeds, streams, and databases
|
||||
- **Topics**: File, web, feed, stream, database ingestion
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Loading data from various sources
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor, RelationExtractor
|
||||
|
||||
- **[Multi-Source Data Integration Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb)**: Advanced patterns for integrating multiple data sources
|
||||
- **Topics**: Multi-source integration, entity resolution, conflict handling
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Building knowledge graphs from diverse data sources
|
||||
ner = NERExtractor()
|
||||
entities = ner.extract(parsed)
|
||||
|
||||
## Step 5: Visualize Your Knowledge Graph
|
||||
rel = RelationExtractor()
|
||||
relationships = rel.extract(parsed, entities=entities)
|
||||
```
|
||||
|
||||
Visualization helps you understand and explore your knowledge graph structure. Semantica supports multiple visualization formats including interactive HTML, static images, and export formats.
|
||||
Each entity gets a type, confidence score, and source reference. Relationships are extracted as typed triplets: `(subject, predicate, object)`.
|
||||
|
||||
**For detailed visualization examples, see:**
|
||||
- **[Visualization Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/16_Visualization.ipynb)**: Learn to create interactive and static visualizations
|
||||
- **Topics**: Network graphs, interactive HTML, static images, export formats
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Exploring graph structure, presentations, analysis
|
||||
---
|
||||
|
||||
- **[Complete Visualization Suite Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/03_Complete_Visualization_Suite.ipynb)**: Advanced visualization techniques
|
||||
- **Topics**: Custom layouts, filtering, styling, multiple graph types
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Production visualizations, custom dashboards
|
||||
## Step 4 — Build the Knowledge Graph
|
||||
|
||||
## Step 6: Export Your Knowledge Graph
|
||||
```python
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
Export your knowledge graph to various formats for integration with other systems or tools. Semantica supports RDF, JSON, CSV, OWL, GraphML, and more.
|
||||
builder = GraphBuilder(merge_entities=True)
|
||||
graph = builder.build(entities=entities, relationships=relationships)
|
||||
|
||||
**Supported export formats:**
|
||||
- **RDF**: Turtle, RDF/XML, JSON-LD, N-Triples
|
||||
- **JSON**: Standard JSON, JSON-LD, Cytoscape.js format
|
||||
- **CSV**: Node and edge lists for spreadsheet tools
|
||||
- **OWL**: OWL/XML and Turtle for ontologies
|
||||
- **Graph Formats**: GraphML, GEXF, DOT for visualization tools
|
||||
print(f"{len(graph.nodes)} nodes, {len(graph.edges)} edges")
|
||||
```
|
||||
|
||||
**For detailed export examples, see:**
|
||||
- **[Export Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/15_Export.ipynb)**: Learn to export to all supported formats
|
||||
- **Topics**: RDF, JSON, CSV, OWL, GraphML export
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Data integration, sharing knowledge graphs
|
||||
`merge_entities=True` resolves duplicates across sources automatically.
|
||||
|
||||
- **[Multi-Format Export Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/05_Multi_Format_Export.ipynb)**: Advanced export patterns
|
||||
- **Topics**: Batch export, custom formats, format conversion
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Production exports, format migration
|
||||
---
|
||||
|
||||
## Step 5 — Visualize
|
||||
|
||||
```python
|
||||
from semantica.visualization import GraphVisualizer
|
||||
|
||||
viz = GraphVisualizer()
|
||||
viz.visualize(graph, output="graph.html") # interactive HTML
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Export
|
||||
|
||||
```python
|
||||
from semantica.export import RDFExporter
|
||||
|
||||
exporter = RDFExporter()
|
||||
rdf = exporter.export_to_rdf(graph, format="turtle")
|
||||
```
|
||||
|
||||
Other formats: `"json-ld"`, `"nt"`, `"xml"`, Parquet, ArangoDB AQL. See [Export Reference](reference/export.md).
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Process Text Directly
|
||||
### Process text directly (no file)
|
||||
|
||||
You can process text directly without file ingestion. This is useful when you already have text content in memory.
|
||||
```python
|
||||
from semantica.semantic_extract import NERExtractor
|
||||
|
||||
**For examples, see:**
|
||||
- **[Entity Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/05_Entity_Extraction.ipynb)**: Processing text directly
|
||||
- **[Building Knowledge Graphs Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/07_Building_Knowledge_Graphs.ipynb)**: Graph construction from text
|
||||
ner = NERExtractor()
|
||||
entities = ner.extract("Apple Inc. was founded by Steve Jobs in 1976.")
|
||||
```
|
||||
|
||||
### Pattern 2: Custom Entity Extraction
|
||||
### Incremental build from multiple sources
|
||||
|
||||
Configure entity extraction with different methods (ML models, LLMs) and parameters for your specific needs.
|
||||
```python
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
**For examples, see:**
|
||||
- **[Entity Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/05_Entity_Extraction.ipynb)**: Different extraction methods and configurations
|
||||
- **[Advanced Extraction Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/01_Advanced_Extraction.ipynb)**: Advanced extraction patterns
|
||||
all_entities, all_rels = [], []
|
||||
for doc in parsed_docs:
|
||||
all_entities.extend(ner.extract(doc))
|
||||
all_rels.extend(rel.extract(doc, entities=all_entities))
|
||||
|
||||
### Pattern 3: Incremental Building
|
||||
graph = GraphBuilder(merge_entities=True).build(
|
||||
entities=all_entities, relationships=all_rels
|
||||
)
|
||||
```
|
||||
|
||||
Build knowledge graphs incrementally from multiple sources and merge them together.
|
||||
|
||||
**For examples, see:**
|
||||
- **[Building Knowledge Graphs Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/07_Building_Knowledge_Graphs.ipynb)**: Graph construction and merging
|
||||
- **[Multi-Source Data Integration Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/06_Multi_Source_Data_Integration.ipynb)**: Advanced integration patterns
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you've built your first knowledge graph:
|
||||
|
||||
1. **[Explore Examples](examples.md)** - See more advanced use cases
|
||||
2. **[API Reference](reference/core.md)** - Learn about all available methods
|
||||
3. **[Cookbook](cookbook.md)** - Interactive Jupyter notebooks
|
||||
4. **[Full Documentation](https://github.com/Hawksight-AI/semantica/blob/main/README.md)** - Comprehensive guide
|
||||
|
||||
### 🍳 Recommended Cookbook Tutorials
|
||||
|
||||
Continue learning with these interactive tutorials:
|
||||
|
||||
- **[Welcome to Semantica](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/01_Welcome_to_Semantica.ipynb)**: Comprehensive introduction to all modules
|
||||
- **Topics**: Framework overview, all modules, architecture, configuration
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 30-45 minutes
|
||||
- **Use Cases**: Understanding the complete framework
|
||||
|
||||
- **[Your First Knowledge Graph](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/08_Your_First_Knowledge_Graph.ipynb)**: Build your first knowledge graph
|
||||
- **Topics**: Entity extraction, relationship extraction, graph construction, visualization
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 20-30 minutes
|
||||
- **Use Cases**: Hands-on practice with complete workflow
|
||||
|
||||
- **[Data Ingestion](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/02_Data_Ingestion.ipynb)**: Learn to ingest from multiple sources
|
||||
- **Topics**: File, web, feed, stream, database ingestion
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Loading data from various sources
|
||||
|
||||
- **[Document Parsing](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/introduction/03_Document_Parsing.ipynb)**: Parse various document formats
|
||||
- **Topics**: PDF, DOCX, HTML, JSON parsing
|
||||
- **Difficulty**: Beginner
|
||||
- **Time**: 15-20 minutes
|
||||
- **Use Cases**: Extracting text from different file formats
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| No entities extracted | Check the document has machine-readable text (not just scanned images) |
|
||||
| Slow processing | Process in chunks; use GPU acceleration (`pip install semantica[gpu]`) |
|
||||
| Memory errors | Reduce batch size or switch to a persistent graph backend |
|
||||
|
||||
**Issue**: No entities extracted
|
||||
- **Solution**: Check that your document contains text content. PDFs with images only won't work without OCR.
|
||||
---
|
||||
|
||||
**Issue**: Slow processing
|
||||
- **Solution**: For large documents, consider processing in chunks or using GPU acceleration.
|
||||
## Next Steps
|
||||
|
||||
**Issue**: Memory errors
|
||||
- **Solution**: Process documents one at a time or reduce batch sizes.
|
||||
|
||||
Need help? Check the [Installation Troubleshooting](installation.md#troubleshooting) or [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues).
|
||||
- [Core Concepts](concepts.md) — understand how knowledge graphs and reasoning work
|
||||
- [Modules Guide](modules.md) — every module explained
|
||||
- [Use Cases](use-cases.md) — domain-specific examples
|
||||
- [Cookbook](cookbook.md) — interactive Jupyter notebooks for each step
|
||||
|
||||
@@ -58,16 +58,14 @@ The Semantica change management module provides enterprise-grade version control
|
||||
|
||||
</div>
|
||||
|
||||
### Key Features
|
||||
|
||||
- ✅ **Enterprise Version Control** — Complete snapshot management with SHA-256 integrity verification
|
||||
- ✅ **Dual Storage Backends** — InMemory (development) and SQLite (production) with ACID guarantees
|
||||
- ✅ **Knowledge Graph Versioning** — Entity and relationship-level change tracking with detailed diffs
|
||||
- ✅ **Ontology Versioning** — Structural change tracking for classes, properties, and axioms
|
||||
- ✅ **Audit Trail Compliance** — Complete change logs with author attribution and timestamps
|
||||
- ✅ **Data Integrity** — SHA-256 checksums for tamper detection and verification
|
||||
- ✅ **Change Comparison** — Detailed diff algorithms for entities, relationships, and ontology structures
|
||||
- ✅ **Backward Compatibility** — Legacy support for existing ontology version management
|
||||
- **Enterprise Version Control** — Complete snapshot management with SHA-256 integrity verification
|
||||
- **Dual Storage Backends** — InMemory (development) and SQLite (production) with ACID guarantees
|
||||
- **Knowledge Graph Versioning** — Entity and relationship-level change tracking with detailed diffs
|
||||
- **Ontology Versioning** — Structural change tracking for classes, properties, and axioms
|
||||
- **Audit Trail Compliance** — Complete change logs with author attribution and timestamps
|
||||
- **Data Integrity** — SHA-256 checksums for tamper detection and verification
|
||||
- **Change Comparison** — Detailed diff algorithms for entities, relationships, and ontology structures
|
||||
- **Backward Compatibility** — Legacy support for existing ontology version management
|
||||
|
||||
---
|
||||
|
||||
@@ -121,24 +119,21 @@ class ChangeLogEntry:
|
||||
|
||||
### Storage Backends
|
||||
|
||||
#### InMemoryVersionStorage
|
||||
Fast, volatile storage for development and testing.
|
||||
**InMemoryVersionStorage** — Fast, volatile storage for development and testing.
|
||||
```python
|
||||
from semantica.change_management import InMemoryVersionStorage
|
||||
|
||||
storage = InMemoryVersionStorage()
|
||||
```
|
||||
|
||||
#### SQLiteVersionStorage
|
||||
Persistent storage with ACID guarantees for production.
|
||||
**SQLiteVersionStorage** — Persistent storage with ACID guarantees for production.
|
||||
```python
|
||||
from semantica.change_management import SQLiteVersionStorage
|
||||
|
||||
storage = SQLiteVersionStorage("versions.db")
|
||||
```
|
||||
|
||||
#### VersionStorage (Abstract)
|
||||
Base interface for custom storage implementations.
|
||||
**VersionStorage (Abstract)** — Base interface for custom storage implementations.
|
||||
|
||||
**Core Methods:**
|
||||
- `save(snapshot)` - Store version snapshot
|
||||
@@ -245,6 +240,50 @@ print(f"Axioms modified: {diff['axioms_modified']}")
|
||||
|
||||
---
|
||||
|
||||
## Incremental / Delta processing
|
||||
|
||||
For large-scale knowledge graphs, reprocessing the entire dataset on every update is computationally expensive.
|
||||
Semantica supports **Delta-Aware Pipelines**, allowing you to compute the exact differences (added and removed triples)
|
||||
between the two graph snapshots and run validation, enrichment, or export jobs *only* on the changes.
|
||||
|
||||
**Delta Pipeline Example**
|
||||
|
||||
```python
|
||||
from semantica.change_management import TemporalVersionManager
|
||||
from semantica.pipeline import PipelineBuilder, ExecutionEngine
|
||||
|
||||
# a. Initialize your managers
|
||||
version_manager = TemporalVersionManager(store_graph="kg_version.db")
|
||||
triplet_store = get_my_triplet_store()
|
||||
|
||||
# b. Build a delta-aware pipeline
|
||||
builder = PipelineBuilder()
|
||||
builder.add_step(
|
||||
step_name="validate_changes",
|
||||
step_type="validation",
|
||||
handler=my_validation_handler,
|
||||
delta_mode=True, # Enables incremental processing
|
||||
base_version_id="v1.0",
|
||||
target_version_id="v1.1",
|
||||
)
|
||||
|
||||
pipeline = builder.build("incremental_nightly_job")
|
||||
|
||||
# c. Execute the pipeline
|
||||
engine = ExecutionEngine()
|
||||
|
||||
# The engine dynamically intercepts the flow, computes the delta on the
|
||||
# database backend, and passes ONLY the changed triples to the handler.
|
||||
result = engine.execute_pipeline(
|
||||
pipeline,
|
||||
data={}, # Is ignored in delta mode
|
||||
version_manager=version_manager,
|
||||
triplet_store=triplet_store
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Integrity
|
||||
|
||||
### compute_checksum
|
||||
@@ -348,3 +387,77 @@ prod_manager = TemporalVersionManager(
|
||||
for version in prod_manager.list_versions():
|
||||
print(f"{version['timestamp']}: {version['description']} by {version['author']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ontology Diff & Migration
|
||||
|
||||
Semantica allows you to treat ontology schema changes with the same rigor as database migrations. By comparing two versions, you can generate a machine-readable diff and a structured impact report to catch breaking changes before they reach production.
|
||||
|
||||
|
||||
**Comparing Versions**
|
||||
|
||||
The `OntologyEngine` provides a high-level API to orchestrate the comparison of two schema versions.
|
||||
|
||||
```python
|
||||
from semantica.ontology.engine import OntologyEngine
|
||||
|
||||
engine = OntologyEngine()
|
||||
|
||||
# Generate a migration impact report between v1.0 and v2.0
|
||||
report = engine.compare_versions(
|
||||
base_id="v1.0",
|
||||
target_id="v2.0"
|
||||
)
|
||||
|
||||
print(f"Total changes detected: {report['summary']['total_changes']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Report Format**
|
||||
|
||||
The `compare_versions` method returns a dictionary with a machine-readable diff and a human-readable impact analysis:
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"total_changes": 12
|
||||
},
|
||||
"impact_classification": {
|
||||
"breaking": [
|
||||
{
|
||||
"entity_uri": "http://example.org/Person",
|
||||
"severity": "critical",
|
||||
"description": "Class Person removed.",
|
||||
"mitigation": "Migrate orphaned instances."
|
||||
}
|
||||
],
|
||||
"potentially_breaking": [],
|
||||
"safe": []
|
||||
},
|
||||
"recommendations": [
|
||||
"[BREAKING] Schedule downtime or validate existing data."
|
||||
],
|
||||
"diff": {
|
||||
"added_classes": [],
|
||||
"removed_classes": [],
|
||||
"changed_classes": [],
|
||||
"added_properties": [],
|
||||
"removed_properties": [],
|
||||
"changed_properties": []
|
||||
},
|
||||
"validation_results": {
|
||||
"valid": true,
|
||||
"consistent": true,
|
||||
"satisfiable": true,
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"graph_validation": {
|
||||
"valid": false,
|
||||
"errors": ["Instance data violates new domain constraint"],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -275,7 +275,7 @@ print(f"Python importance score: {importance.get('degree', 0)}")
|
||||
|--------|-------------|------------|
|
||||
| `add_node(node_id, node_type, properties)` | Add concepts to remember | Build knowledge base |
|
||||
| `add_edge(source, target, relation)` | Connect related concepts | Show relationships |
|
||||
| `add_decision(category, scenario, reasoning, outcome, confidence, ...)` | Record decisions | Track choices and learn |
|
||||
| `add_decision(decision)` or `add_decision(category, scenario, reasoning, outcome, ...)` | Record decisions | Track choices and learn |
|
||||
| `add_decision_simple(category, scenario, reasoning, outcome, confidence, ...)` | Easy decision recording | Quick decision tracking |
|
||||
| `find_precedents(decision_id, limit)` | Find precedents by ID | Get connected decisions |
|
||||
| `find_precedents_by_scenario(scenario, category, ...)` | Find similar decisions | Make consistent choices |
|
||||
|
||||
+159
-1
@@ -250,6 +250,56 @@ ontology:
|
||||
|
||||
---
|
||||
|
||||
## Ontology Alignment
|
||||
|
||||
Semantica supports mapping and connecting different ontologies to unify data across systems, standards, and domains. This enables cross-system interoperability, allowing a single semantic layer to span multiple standards (e.g., internal models and industry standards).
|
||||
|
||||
Alignments are represented using standard RDF predicates such as `owl:equivalentClass`, `owl:equivalentProperty`, and `skos:exactMatch`.
|
||||
|
||||
### Creating and Managing Alignments
|
||||
|
||||
You can create and query alignments programmatically using the `OntologyEngine`:
|
||||
|
||||
```python
|
||||
from semantica.ontology.engine import OntologyEngine
|
||||
from semantica.triplet_store.triplet_store import TripletStore
|
||||
|
||||
# Setup the store and engine (using Blazegraph as an example)
|
||||
my_triplet_store = TripletStore(backend="blazegraph")
|
||||
engine = OntologyEngine(store=my_triplet_store)
|
||||
|
||||
# Create an alignment between an internal class and a standard schema
|
||||
engine.create_alignment(
|
||||
source_uri="http://internal.org/ontology/Employee",
|
||||
target_uri="http://schema.org/Person",
|
||||
predicate="http://www.w3.org/2002/07/owl#equivalentClass"
|
||||
)
|
||||
|
||||
# Retrieve all bidirectional alignments for a specific entity
|
||||
alignments = engine.get_alignments("http://internal.org/ontology/Employee")
|
||||
```
|
||||
### Automated Alignment Suggestions
|
||||
|
||||
When importing or merging external ontologies, the ReuseManager can automatically suggest alignments based on heuristic matching (such as identical labels with differing URIs).
|
||||
|
||||
```python
|
||||
from semantica.ontology.reuse_manager import ReuseManager
|
||||
|
||||
manager = ReuseManager()
|
||||
|
||||
# Merge ontologies and auto-compute alignment suggestions
|
||||
merged_ontology = manager.merge_ontology_data(
|
||||
target=internal_ontology,
|
||||
source=industry_ontology,
|
||||
compute_alignments=True
|
||||
)
|
||||
|
||||
# Suggestions are stored in merged_ontology["suggested_alignments"]
|
||||
```
|
||||
|
||||
For executing SPARQL queries that utilize these alignments to retrieve cross-ontology results, see the [Triplet Store Alignment-Aware Queries](triplet_store.md#alignment-aware-queries)
|
||||
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Schema-First Knowledge Graph
|
||||
@@ -285,6 +335,114 @@ else:
|
||||
|
||||
---
|
||||
|
||||
## SKOS Vocabulary Management
|
||||
|
||||
Semantica supports [SKOS (Simple Knowledge Organization System)](https://www.w3.org/TR/skos-reference/) vocabularies as first-class semantic assets. SKOS triples are stored in the existing RDF triplet store and queried through the `OntologyEngine` — no additional packages are required.
|
||||
|
||||
### Concepts and data model
|
||||
|
||||
| SKOS element | RDF type / predicate |
|
||||
|---|---|
|
||||
| ConceptScheme | `skos:ConceptScheme` |
|
||||
| Concept | `skos:Concept` |
|
||||
| Preferred label | `skos:prefLabel` |
|
||||
| Alternative label | `skos:altLabel` |
|
||||
| Broader concept | `skos:broader` |
|
||||
| Narrower concept | `skos:narrower` |
|
||||
| Related concept | `skos:related` |
|
||||
| Human definition | `skos:definition` |
|
||||
| Notation / code | `skos:notation` |
|
||||
|
||||
### Importing a SKOS vocabulary
|
||||
|
||||
Use `TripletStore.add_skos_concept()` to load individual concepts. The method automatically asserts the parent `skos:ConceptScheme` triple the first time any concept for that scheme is added.
|
||||
|
||||
```python
|
||||
from semantica.triplet_store import TripletStore
|
||||
|
||||
store = TripletStore(backend="blazegraph", endpoint="http://localhost:9999/blazegraph")
|
||||
|
||||
SCHEME = "https://vocab.example.org/colours"
|
||||
|
||||
store.add_skos_concept(
|
||||
concept_uri="https://vocab.example.org/colours/red",
|
||||
scheme_uri=SCHEME,
|
||||
pref_label="Red",
|
||||
alt_labels=["Crimson", "Rouge"],
|
||||
broader=["https://vocab.example.org/colours/warm"],
|
||||
definition="The colour at the long-wavelength end of the visible spectrum.",
|
||||
notation="RED",
|
||||
)
|
||||
|
||||
store.add_skos_concept(
|
||||
concept_uri="https://vocab.example.org/colours/blue",
|
||||
scheme_uri=SCHEME,
|
||||
pref_label="Blue",
|
||||
alt_labels=["Azure", "Cerulean"],
|
||||
)
|
||||
```
|
||||
|
||||
For bulk ingestion of an existing SKOS/Turtle file use `TripletStore.add_triplets()` after parsing the file with [rdflib](https://rdflib.readthedocs.io/):
|
||||
|
||||
```python
|
||||
import rdflib
|
||||
from semantica.semantic_extract.triplet_extractor import Triplet
|
||||
|
||||
g = rdflib.Graph()
|
||||
g.parse("my_vocabulary.ttl", format="turtle")
|
||||
|
||||
triplets = [
|
||||
Triplet(subject=str(s), predicate=str(p), object=str(o))
|
||||
for s, p, o in g
|
||||
]
|
||||
store.add_triplets(triplets)
|
||||
```
|
||||
|
||||
### Listing and searching concepts
|
||||
|
||||
Once a vocabulary is loaded, use `OntologyEngine` to browse and search it:
|
||||
|
||||
```python
|
||||
from semantica.ontology import OntologyEngine
|
||||
|
||||
engine = OntologyEngine(store=store)
|
||||
|
||||
# 1. List all ConceptSchemes in the store
|
||||
vocabularies = engine.list_vocabularies()
|
||||
# [{"uri": "https://vocab.example.org/colours", "label": "Colours"}, ...]
|
||||
|
||||
# 2. List every concept in a specific scheme
|
||||
concepts = engine.list_concepts("https://vocab.example.org/colours")
|
||||
# [{"uri": "...", "pref_label": "Red", "alt_labels": ["Crimson", "Rouge"]}, ...]
|
||||
|
||||
# 3. Case-insensitive substring search across prefLabel and altLabel
|
||||
results = engine.search_concepts("crimson")
|
||||
# [{"uri": "https://vocab.example.org/colours/red", "label": "Crimson"}]
|
||||
|
||||
# 4. Restrict search to one scheme
|
||||
results = engine.search_concepts("azure", scheme_uri="https://vocab.example.org/colours")
|
||||
```
|
||||
|
||||
### Building SKOS URIs with NamespaceManager
|
||||
|
||||
`NamespaceManager` provides helpers for constructing well-formed SKOS IRIs:
|
||||
|
||||
```python
|
||||
from semantica.ontology import NamespaceManager
|
||||
|
||||
nm = NamespaceManager(base_uri="https://vocab.example.org/")
|
||||
|
||||
# Full SKOS predicate URI
|
||||
nm.get_skos_uri("prefLabel")
|
||||
# "http://www.w3.org/2004/02/skos/core#prefLabel"
|
||||
|
||||
# Slug-based ConceptScheme URI anchored at the base
|
||||
nm.build_concept_scheme_uri("ISO 3166 Countries")
|
||||
# "https://vocab.example.org/vocab/iso-3166-countries"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Reuse Standard Ontologies**: Don't reinvent `Person` or `Organization`; import FOAF or Schema.org using `ReuseManager`.
|
||||
@@ -312,4 +470,4 @@ Interactive tutorials to learn ontology generation and management:
|
||||
- **[Unstructured to Ontology](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/advanced/12_Unstructured_to_Ontology.ipynb)**: Generate ontologies automatically from unstructured data
|
||||
- **Topics**: Automatic ontology generation, 6-stage pipeline, OWL validation
|
||||
- **Difficulty**: Advanced
|
||||
- **Use Cases**: Domain modeling, automatic schema generation
|
||||
- **Use Cases**: Domain modeling, automatic schema generation
|
||||
@@ -131,7 +131,7 @@ The **Pipeline Module** provides a robust orchestration engine for building, exe
|
||||
### Types
|
||||
|
||||
- `Pipeline` — Pipeline definition dataclass
|
||||
- `PipelineStep` — Pipeline step definition dataclass
|
||||
- `PipelineStep` — Pipeline step definition dataclass, Supports `delta_mode` (bool), `base_version_id` (str), and `target_version_id` (str) for incremental processing.
|
||||
- `StepStatus` — Enum: `pending`, `running`, `completed`, `failed`, `skipped`
|
||||
- `ExecutionResult` — Execution result dataclass
|
||||
- `PipelineStatus` — Enum: `pending`, `running`, `paused`, `completed`, `failed`, `stopped`
|
||||
@@ -405,6 +405,47 @@ result = engine.execute_pipeline(pipeline, data={"path": "document.pdf"})
|
||||
|
||||
---
|
||||
|
||||
### Incremental / Delta-Aware Pipeline
|
||||
|
||||
Use `delta_mode` to process only the differences between two graph versions, drastically reducing compute costs for large datasets.
|
||||
|
||||
```python
|
||||
from semantica.pipeline import PipelineBuilder, ExecutionEngine
|
||||
|
||||
builder = (
|
||||
PipelineBuilder()
|
||||
# Adding delta_mode=True tells the execution engine to intercept this step,
|
||||
# compute the diff between v1 and v2, and pass ONLY the delta payload to the handler.
|
||||
.add_step(
|
||||
"validate_diff",
|
||||
"validation",
|
||||
delta_mode=True,
|
||||
base_version_id="v1",
|
||||
target_version_id="v2",
|
||||
handler=diff_validator
|
||||
)
|
||||
.add_step(
|
||||
"alert_on_removals",
|
||||
"alerting",
|
||||
dependencies=["validate_diff"],
|
||||
handler=alert_handler
|
||||
)
|
||||
)
|
||||
|
||||
pipeline = builder.build(name="IncrementalJob")
|
||||
engine = ExecutionEngine()
|
||||
|
||||
# Execution requires version_manager and triplet_store injected via options
|
||||
# so the engine can resolve URIs and compute the graph differences natively.
|
||||
result = engine.execute_pipeline(
|
||||
pipeline,
|
||||
version_manager=my_version_manager,
|
||||
triplet_store=my_triplet_store
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Idempotency**: Ensure steps are idempotent (can be run multiple times without side effects) to support retries.
|
||||
|
||||
@@ -734,4 +734,4 @@ MIT License - See [LICENSE](../../LICENSE) for details.
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions, please open an issue on GitHub or join our [Discord](https://discord.gg/N7WmAuDH).
|
||||
For issues or questions, please open an issue on GitHub or join our [Discord](https://discord.gg/sV34vps5hH).
|
||||
|
||||
@@ -152,6 +152,8 @@ SPARQL query execution and optimization engine.
|
||||
|--------|-------------|-----------|
|
||||
| `execute(query)` | Execute SPARQL query | Query execution |
|
||||
| `optimize(query)` | Optimize SPARQL query | Query rewriting |
|
||||
| `expand_entity_uri(uri, store, ...)` | Expand aligned entity URIs | Bidirectional SPARQL lookup |
|
||||
| `build_values_clause(var, uris)` | Generate VALUES clause | String formatting |
|
||||
|
||||
---
|
||||
|
||||
@@ -204,3 +206,79 @@ LIMIT 10
|
||||
"""
|
||||
results = store.execute_query(query)
|
||||
```
|
||||
|
||||
### Named Graph Partitions
|
||||
|
||||
Use named graphs to partition RDF data inside one store while keeping backward compatibility.
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract.triplet_extractor import Triplet
|
||||
|
||||
# Write into a specific graph partition
|
||||
store.add_triplet(
|
||||
Triplet("http://entity/1", "http://relation/type", "http://TypeA"),
|
||||
graph="http://example.org/graphs/partition-a",
|
||||
)
|
||||
|
||||
# Query only one graph as default dataset
|
||||
result_a = store.execute_query(
|
||||
"SELECT ?s ?p ?o WHERE { ?s ?p ?o }",
|
||||
graph="http://example.org/graphs/partition-a",
|
||||
)
|
||||
|
||||
# Query multiple named graphs (use GRAPH pattern in WHERE)
|
||||
result_multi = store.execute_query(
|
||||
"""
|
||||
SELECT ?g ?s ?p ?o WHERE {
|
||||
GRAPH ?g { ?s ?p ?o }
|
||||
}
|
||||
""",
|
||||
graphs=[
|
||||
"http://example.org/graphs/partition-a",
|
||||
"http://example.org/graphs/partition-b",
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `graph` injects `FROM <...>` before `WHERE`.
|
||||
- `graphs` injects `FROM NAMED <...>` before `WHERE`.
|
||||
- If not provided, existing behavior is unchanged.
|
||||
|
||||
### Alignment-Aware Queries
|
||||
|
||||
In complex enterprise environments with multiple data sources, you may want queries to seamlessly retrieve instances across aligned classes. For example, retrieving all http://schema.org/Person instances when querying for your internal http://internal.org/ontology/Employee class.
|
||||
|
||||
The QueryEngine provides helper methods to expand entity URIs based on stored alignments (e.g., owl:equivalentClass, owl:sameAs, skos:exactMatch) and safely inject them into your queries using SPARQL VALUES clauses.
|
||||
|
||||
Expanding URIs in Queries
|
||||
You can expand a URI and build an alignment-aware query dynamically:
|
||||
|
||||
```python
|
||||
from semantica.triplet_store.query_engine import QueryEngine
|
||||
|
||||
engine = QueryEngine()
|
||||
|
||||
# i) Expand the base URI to include all aligned equivalents
|
||||
expanded_uris = engine.expand_entity_uri(
|
||||
entity_uri="[http://internal.org/ontology/Employee](http://internal.org/ontology/Employee)",
|
||||
store_backend=store_backend,
|
||||
use_alignments=True
|
||||
)
|
||||
|
||||
# ii) Build a SPARQL VALUES clause
|
||||
values_clause = engine.build_values_clause("entity_class", expanded_uris)
|
||||
# Result: VALUES ?entity_class { [http://internal.org/ontology/Employee](http://internal.org/ontology/Employee) [http://schema.org/Person](http://schema.org/Person) }
|
||||
|
||||
# iii) Inject the clause into your query template
|
||||
query = f"""
|
||||
SELECT ?instance ?name WHERE {{
|
||||
{values_clause}
|
||||
?instance a ?entity_class .
|
||||
?instance [http://schema.org/name](http://schema.org/name) ?name .
|
||||
}}
|
||||
"""
|
||||
|
||||
# Execute the query to retrieve results across all aligned ontologies
|
||||
results = engine.execute_query(query, store_backend)
|
||||
```
|
||||
|
||||
+70
-155
@@ -1,210 +1,125 @@
|
||||
# Use Cases
|
||||
|
||||
Semantica is designed to solve complex data challenges across various domains. This guide explores common use cases and how to implement them.
|
||||
|
||||
!!! info "About This Guide"
|
||||
This guide provides detailed implementation guides for real-world use cases, complete with code examples, prerequisites, and step-by-step instructions.
|
||||
Real-world applications of Semantica across domains, with linked cookbook notebooks for each.
|
||||
|
||||
---
|
||||
|
||||
## Use Case Comparison
|
||||
## Overview
|
||||
|
||||
| Use Case | Difficulty | Time | Domain | Key Features | Cookbook |
|
||||
| :-------------------------------- | :------------ | :---------- | :---------- | :---------------------------------------------- | :------------------------------------------ |
|
||||
| **Biomedical Knowledge Graphs** | Intermediate | 1-2 hours | Healthcare | Gene-protein-disease relationships | Drug Discovery, Genomic Variant Analysis |
|
||||
| **Financial Data Integration** | Intermediate | 1-2 hours | Finance | MCP integration, real-time data | Financial Data Integration MCP |
|
||||
| **Fraud Detection** | Advanced | 2-3 hours | Finance | Temporal graphs, pattern detection | Fraud Detection |
|
||||
| **Blockchain Analytics** | Intermediate | 1-2 hours | Finance | Transaction tracing, DeFi intelligence | DeFi Protocol Intelligence, Transaction Network |
|
||||
| **Cybersecurity Threat Intelligence**| Advanced | 2-3 hours | Security | Threat mapping, anomaly detection | Real-Time Anomaly Detection, Threat Intelligence |
|
||||
| **Intelligence Analysis** | Intermediate | 1-2 hours | Security | Criminal networks, OSINT analysis | Criminal Network Analysis, Intelligence Orchestrator |
|
||||
| **Supply Chain Optimization** | Intermediate | 1-2 hours | Industry | Data integration, route optimization | Supply Chain Data Integration |
|
||||
| **Renewable Energy Management** | Intermediate | 1-2 hours | Energy | Energy market analysis, optimization | Energy Market Analysis |
|
||||
| **GraphRAG** | Advanced | 1-2 hours | AI | Enhanced RAG with knowledge graphs | GraphRAG Complete, RAG vs GraphRAG |
|
||||
| Use Case | Domain | Difficulty | Estimated Time |
|
||||
|----------|--------|------------|----------------|
|
||||
| Biomedical Knowledge Graphs | Healthcare | Intermediate | 1–2 hours |
|
||||
| Financial Data Integration | Finance | Intermediate | 1–2 hours |
|
||||
| Fraud Detection | Finance | Advanced | 2–3 hours |
|
||||
| Blockchain Analytics | Finance | Intermediate | 1–2 hours |
|
||||
| Cybersecurity Threat Intelligence | Security | Advanced | 2–3 hours |
|
||||
| Criminal Network Analysis | Security / Intelligence | Intermediate | 1–2 hours |
|
||||
| Intelligence Analysis Orchestrator | Intelligence | Intermediate | 1–2 hours |
|
||||
| Supply Chain Optimization | Operations | Intermediate | 1–2 hours |
|
||||
| Renewable Energy Management | Energy | Intermediate | 1–2 hours |
|
||||
| GraphRAG | AI | Advanced | 1–2 hours |
|
||||
|
||||
**Difficulty Levels**:
|
||||
- **Beginner**: Basic Semantica knowledge required
|
||||
- **Intermediate**: Some domain knowledge helpful
|
||||
- **Advanced**: Requires domain expertise and advanced Semantica features
|
||||
**Difficulty levels**:
|
||||
|
||||
- **Beginner** — basic Semantica knowledge only
|
||||
- **Intermediate** — some domain knowledge helpful
|
||||
- **Advanced** — domain expertise + advanced Semantica features
|
||||
|
||||
---
|
||||
|
||||
## Research & Science
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
### Biomedical Knowledge Graphs
|
||||
|
||||
- :material-dna: **Biomedical Knowledge Graphs**
|
||||
---
|
||||
Accelerate drug discovery and understand disease pathways by connecting genes, proteins, drugs, and diseases.
|
||||
|
||||
**Goal**: Connect genes, proteins, drugs, and diseases from scientific literature and databases.
|
||||
|
||||
**Difficulty**: Intermediate
|
||||
|
||||
[:material-arrow-right: Drug Discovery Pipeline](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/biomedical/01_Drug_Discovery_Pipeline.ipynb)
|
||||
|
||||
[:material-arrow-right: Genomic Variant Analysis](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/biomedical/02_Genomic_Variant_Analysis.ipynb)
|
||||
Connect genes, proteins, drugs, and diseases from scientific literature and databases to accelerate drug discovery and understand disease pathways.
|
||||
|
||||
</div>
|
||||
|
||||
### Biomedical Knowledge Graphs Implementation
|
||||
|
||||
**Prerequisites**:
|
||||
- Domain knowledge of biomedical concepts
|
||||
- Access to biomedical literature/databases
|
||||
|
||||
**Implementation Guides:**
|
||||
|
||||
- **[Drug Discovery Pipeline Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/biomedical/01_Drug_Discovery_Pipeline.ipynb)**: Build knowledge graphs from PubMed RSS feeds
|
||||
- **Topics**: PubMed RSS ingestion, entity-aware chunking, GraphRAG, vector similarity search
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 1-2 hours
|
||||
- **Use Cases**: Drug discovery, biomedical research
|
||||
|
||||
- **[Genomic Variant Analysis Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/biomedical/02_Genomic_Variant_Analysis.ipynb)**: Analyze genomic variants using temporal knowledge graphs
|
||||
- **Topics**: bioRxiv RSS, temporal KGs, deduplication, pathway analysis
|
||||
- **Difficulty**: Intermediate
|
||||
- **Time**: 1-2 hours
|
||||
- **Use Cases**: Genomic research, variant analysis
|
||||
**Cookbooks**:
|
||||
- [Drug Discovery Pipeline](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/biomedical/01_Drug_Discovery_Pipeline.ipynb) — PubMed RSS ingestion, entity-aware chunking, GraphRAG, vector similarity search
|
||||
- [Genomic Variant Analysis](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/biomedical/02_Genomic_Variant_Analysis.ipynb) — bioRxiv RSS, temporal KGs, deduplication, pathway analysis
|
||||
|
||||
---
|
||||
|
||||
## Finance & Trading
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
### Financial Data Integration
|
||||
|
||||
- :material-finance: **Financial Data Integration**
|
||||
---
|
||||
Integrate financial data from multiple sources using MCP servers and real-time ingestion.
|
||||
|
||||
**Goal**: Connect Alpha Vantage API, MCP servers, seed data, and real-time ingestion for comprehensive financial analysis.
|
||||
|
||||
[:material-arrow-right: View Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/finance/01_Financial_Data_Integration_MCP.ipynb)
|
||||
Unify financial data from APIs, MCP servers, and real-time streams into a single queryable knowledge graph.
|
||||
|
||||
- :material-shield-alert: **Fraud Detection**
|
||||
---
|
||||
Detect complex fraud rings using temporal knowledge graphs and pattern detection.
|
||||
|
||||
**Goal**: Build a graph of Users, Devices, IP Addresses, and Transactions to find cycles and detect fraud patterns.
|
||||
|
||||
[:material-arrow-right: View Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/finance/02_Fraud_Detection.ipynb)
|
||||
**Cookbook**: [Financial Data Integration (MCP)](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/finance/01_Financial_Data_Integration_MCP.ipynb) — Alpha Vantage API, MCP servers, seed data, real-time ingestion
|
||||
|
||||
- :material-bitcoin: **Blockchain Analytics**
|
||||
---
|
||||
Analyze DeFi protocols and transaction networks for intelligence and fraud detection.
|
||||
|
||||
**Goal**: Map transaction flows between wallets and exchanges, analyze DeFi protocols, and detect illicit activity.
|
||||
|
||||
[:material-arrow-right: DeFi Protocol Intelligence](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/blockchain/01_DeFi_Protocol_Intelligence.ipynb)
|
||||
|
||||
[:material-arrow-right: Transaction Network Analysis](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/blockchain/02_Transaction_Network_Analysis.ipynb)
|
||||
### Fraud Detection
|
||||
|
||||
</div>
|
||||
Detect complex fraud rings using temporal graphs and pattern detection over transaction, device, and user data.
|
||||
|
||||
---
|
||||
**Cookbook**: [Fraud Detection](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/finance/02_Fraud_Detection.ipynb) — temporal KGs, cycle detection, fraud pattern analysis
|
||||
|
||||
### Blockchain Analytics
|
||||
|
||||
Map transaction flows, analyze DeFi protocols, and detect illicit activity across wallet and exchange networks.
|
||||
|
||||
**Cookbooks**:
|
||||
- [DeFi Protocol Intelligence](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/blockchain/01_DeFi_Protocol_Intelligence.ipynb)
|
||||
- [Transaction Network Analysis](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/blockchain/02_Transaction_Network_Analysis.ipynb)
|
||||
|
||||
---
|
||||
|
||||
## Security & Intelligence
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
### Cybersecurity Threat Intelligence
|
||||
|
||||
- :material-shield-lock: **Cybersecurity Threat Intelligence**
|
||||
---
|
||||
Proactively identify and mitigate cyber threats using real-time anomaly detection and threat intelligence.
|
||||
|
||||
**Goal**: Ingest threat feeds (CVE databases, security RSS), detect anomalies in streaming data, and build threat intelligence knowledge graphs.
|
||||
|
||||
[:material-arrow-right: Real-Time Anomaly Detection](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/cybersecurity/01_Real_Time_Anomaly_Detection.ipynb)
|
||||
|
||||
[:material-arrow-right: Threat Intelligence Hybrid RAG](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/cybersecurity/02_Threat_Intelligence_Hybrid_RAG.ipynb)
|
||||
Ingest threat feeds (CVE databases, security RSS), detect anomalies in streaming data, and build threat intelligence knowledge graphs for proactive defense.
|
||||
|
||||
- :material-account-network: **Criminal Network Analysis**
|
||||
---
|
||||
Analyze criminal networks to identify key players, communities, and suspicious patterns using OSINT RSS feeds, deduplication, and network centrality analysis.
|
||||
|
||||
**Goal**: Build knowledge graphs from police reports, court records, and surveillance data.
|
||||
|
||||
[:material-arrow-right: View Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/intelligence/01_Criminal_Network_Analysis.ipynb)
|
||||
**Cookbooks**:
|
||||
- [Real-Time Anomaly Detection](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/cybersecurity/01_Real_Time_Anomaly_Detection.ipynb)
|
||||
- [Threat Intelligence Hybrid RAG](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/cybersecurity/02_Threat_Intelligence_Hybrid_RAG.ipynb)
|
||||
|
||||
- :material-file-search: **Intelligence Analysis Orchestrator Worker**
|
||||
---
|
||||
Comprehensive intelligence analysis using pipeline orchestrator with multiple RSS feeds, conflict detection, and multi-source integration.
|
||||
|
||||
**Goal**: Process multiple intelligence sources in parallel using orchestrator-worker pattern.
|
||||
|
||||
[:material-arrow-right: View Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/intelligence/02_Intelligence_Analysis_Orchestrator_Worker.ipynb)
|
||||
### Criminal Network Analysis
|
||||
|
||||
</div>
|
||||
Build knowledge graphs from police reports, court records, and OSINT feeds to identify key players, communities, and suspicious patterns using network centrality analysis.
|
||||
|
||||
**Cookbook**: [Criminal Network Analysis](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/intelligence/01_Criminal_Network_Analysis.ipynb)
|
||||
|
||||
### Intelligence Analysis Orchestrator
|
||||
|
||||
Process multiple intelligence sources in parallel using an orchestrator-worker pipeline pattern with multi-source conflict detection and integration.
|
||||
|
||||
**Cookbook**: [Intelligence Analysis Orchestrator-Worker](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/intelligence/02_Intelligence_Analysis_Orchestrator_Worker.ipynb)
|
||||
|
||||
---
|
||||
|
||||
## Industry & Operations
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
### Supply Chain Optimization
|
||||
|
||||
- :material-truck-delivery: **Supply Chain Optimization**
|
||||
---
|
||||
Visualize and optimize complex global supply chains.
|
||||
|
||||
**Goal**: Map suppliers, logistics routes, and inventory levels to identify bottlenecks.
|
||||
|
||||
[:material-arrow-right: View Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/supply_chain/01_Supply_Chain_Data_Integration.ipynb)
|
||||
Map suppliers, logistics routes, and inventory levels to identify bottlenecks and optimize global supply chains.
|
||||
|
||||
- :material-wind-turbine: **Renewable Energy Management**
|
||||
---
|
||||
Optimize grid operations and asset maintenance.
|
||||
|
||||
**Goal**: Connect sensor data, weather forecasts, and maintenance logs to predict failures.
|
||||
|
||||
[:material-arrow-right: View Cookbook](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/renewable_energy/01_Energy_Market_Analysis.ipynb)
|
||||
**Cookbook**: [Supply Chain Data Integration](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/supply_chain/01_Supply_Chain_Data_Integration.ipynb)
|
||||
|
||||
</div>
|
||||
### Renewable Energy Management
|
||||
|
||||
Connect sensor data, weather forecasts, and maintenance logs to predict equipment failures and optimize grid operations.
|
||||
|
||||
**Cookbook**: [Energy Market Analysis](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/renewable_energy/01_Energy_Market_Analysis.ipynb)
|
||||
|
||||
---
|
||||
|
||||
## Advanced AI Patterns
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
### GraphRAG (Graph-Augmented Generation)
|
||||
|
||||
- :material-robot: **Graph-Augmented Generation (GraphRAG)**
|
||||
---
|
||||
Enhance LLM responses with structured ground truth using knowledge graphs.
|
||||
|
||||
**Goal**: Use the knowledge graph to retrieve precise context for RAG applications with hybrid retrieval and logical inference.
|
||||
|
||||
[:material-arrow-right: GraphRAG Complete](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb)
|
||||
|
||||
[:material-scale-balance: RAG vs GraphRAG Comparison](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb)
|
||||
Use knowledge graphs to retrieve precise, structured context for LLM responses — with hybrid retrieval, logical inference, and source attribution.
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This guide covered use cases across multiple domains with corresponding cookbooks:
|
||||
|
||||
- **Research & Science**: Biomedical knowledge graphs (Drug Discovery, Genomic Variant Analysis)
|
||||
- **Finance & Trading**: Financial data integration, fraud detection, blockchain analytics
|
||||
- **Security & Intelligence**: Cybersecurity threat intelligence, criminal network analysis, intelligence orchestration
|
||||
- **Industry**: Supply chain optimization, renewable energy management
|
||||
- **AI Applications**: GraphRAG (Complete implementation and comparison)
|
||||
**Cookbooks**:
|
||||
- [GraphRAG Complete](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/01_GraphRAG_Complete.ipynb) — production-ready implementation
|
||||
- [RAG vs. GraphRAG Comparison](https://github.com/Hawksight-AI/semantica/blob/main/cookbook/use_cases/advanced_rag/02_RAG_vs_GraphRAG_Comparison.ipynb) — side-by-side comparison
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Examples](examples.md)** - More detailed code examples
|
||||
- **[Modules Guide](modules.md)** - Learn about available modules
|
||||
- **[Cookbook](cookbook.md)** - Interactive Jupyter notebooks
|
||||
- **[API Reference](reference/core.md)** - Complete API documentation
|
||||
|
||||
---
|
||||
|
||||
!!! info "Contribute"
|
||||
Have a use case to add? [Contribute on GitHub](https://github.com/Hawksight-AI/semantica)
|
||||
- [Cookbook](cookbook.md) — full notebook catalog organized by topic and difficulty
|
||||
- [Modules Guide](modules.md) — every module with examples
|
||||
- [API Reference](reference/core.md) — complete technical documentation
|
||||
|
||||
!!! info "Have a use case to add?"
|
||||
[Open a PR](https://github.com/Hawksight-AI/semantica) or start a discussion on GitHub.
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
"""
|
||||
Capability Gap Analysis with Semantica Context Graphs
|
||||
|
||||
This example mirrors the military capability-gap notebook as a runnable Python script.
|
||||
It uses Semantica modules and classes across ingestion, parsing, ontology handling,
|
||||
splitting, normalization, semantic extraction, KG analytics, context graphs,
|
||||
reasoning, provenance, and export.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from semantica.change_management import VersionManager
|
||||
from semantica.conflicts import detect_conflicts, resolve_conflicts, voting
|
||||
from semantica.context import (
|
||||
AgentContext,
|
||||
ContextGraph,
|
||||
Decision,
|
||||
Policy,
|
||||
PolicyEngine,
|
||||
multi_hop_query,
|
||||
)
|
||||
from semantica.export import (
|
||||
ReportGenerator,
|
||||
export_csv,
|
||||
export_graph,
|
||||
export_json,
|
||||
export_lpg,
|
||||
export_rdf,
|
||||
export_yaml,
|
||||
)
|
||||
from semantica.ingest import FileIngestor, OntologyIngestor, ingest_web
|
||||
from semantica.kg import (
|
||||
CentralityCalculator,
|
||||
CommunityDetector,
|
||||
ConnectivityAnalyzer,
|
||||
EntityResolver,
|
||||
GraphAnalyzer,
|
||||
GraphBuilder,
|
||||
LinkPredictor,
|
||||
NodeEmbedder,
|
||||
SimilarityCalculator,
|
||||
)
|
||||
from semantica.normalize import (
|
||||
clean_text,
|
||||
detect_language,
|
||||
handle_encoding,
|
||||
normalize_text,
|
||||
)
|
||||
from semantica.ontology import OntologyEvaluator, ingest_ontology
|
||||
from semantica.parse import (
|
||||
DOCLING_AVAILABLE,
|
||||
DocumentParser,
|
||||
DoclingParser,
|
||||
PDFParser,
|
||||
parse_document,
|
||||
parse_pdf,
|
||||
)
|
||||
from semantica.pipeline import PipelineBuilder
|
||||
from semantica.provenance import ProvenanceManager
|
||||
from semantica.reasoning import ExplanationGenerator, Reasoner
|
||||
from semantica.semantic_extract import (
|
||||
CoreferenceResolver,
|
||||
EventDetector,
|
||||
ExtractionValidator,
|
||||
NamedEntityRecognizer,
|
||||
RelationExtractor,
|
||||
SemanticAnalyzer,
|
||||
SemanticNetworkExtractor,
|
||||
TripletExtractor,
|
||||
)
|
||||
from semantica.split import TextSplitter
|
||||
from semantica.vector_store import VectorStore
|
||||
from semantica.visualization import KGVisualizer
|
||||
|
||||
|
||||
def build_paths() -> tuple[Path, Path, Path]:
|
||||
# Keep paths workspace-relative so the script is portable across machines.
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
use_case_dir = repo_root / "cookbook" / "use_cases" / "capability_gap_defense"
|
||||
data_dir = use_case_dir / "data"
|
||||
output_dir = use_case_dir / "outputs_py"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
return data_dir, use_case_dir, output_dir
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# -------------------------------------------------------------------------
|
||||
# Workspace setup
|
||||
# -------------------------------------------------------------------------
|
||||
data_dir, use_case_dir, output_dir = build_paths()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 1) Ingestion (Semantica ingest)
|
||||
# -------------------------------------------------------------------------
|
||||
# Local file inventory for PDFs and ontology artifacts.
|
||||
file_ingestor = FileIngestor()
|
||||
file_objects = file_ingestor.ingest_directory(data_dir, recursive=False, read_content=False)
|
||||
|
||||
# Web source ingestion through Semantica's `ingest_web` method wrapper.
|
||||
web_sources = [
|
||||
"https://www.rand.org/pubs/research_reports/RRA733-1.html",
|
||||
"https://foundationcapital.com/context-graphs/",
|
||||
]
|
||||
web_contents = []
|
||||
for url in web_sources:
|
||||
try:
|
||||
web_contents.append(ingest_web(url, method="url"))
|
||||
except Exception as exc:
|
||||
print(f"Web ingestion failed for {url}: {exc}")
|
||||
|
||||
# TTL ontology ingestion from the same data directory.
|
||||
ontology_ingestor = OntologyIngestor()
|
||||
ontology_data = ontology_ingestor.ingest_directory(data_dir, recursive=False)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 2) Ontology introspection + evaluation (Semantica ontology)
|
||||
# -------------------------------------------------------------------------
|
||||
# Capture file-level schema details to verify class/property coverage.
|
||||
ontology_details = []
|
||||
for ttl_file in sorted(data_dir.glob("*.ttl")):
|
||||
try:
|
||||
od = ingest_ontology(ttl_file, method="file")
|
||||
if isinstance(od, list):
|
||||
for item in od:
|
||||
ontology_details.append(
|
||||
{
|
||||
"file": ttl_file.name,
|
||||
"classes": len(item.data.get("classes", [])),
|
||||
"properties": len(item.data.get("properties", [])),
|
||||
}
|
||||
)
|
||||
else:
|
||||
ontology_details.append(
|
||||
{
|
||||
"file": ttl_file.name,
|
||||
"classes": len(od.data.get("classes", [])),
|
||||
"properties": len(od.data.get("properties", [])),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
ontology_details.append({"file": ttl_file.name, "error": str(exc)})
|
||||
|
||||
# Run competency-question driven evaluation over one ontology payload.
|
||||
ontology_eval_result = None
|
||||
if ontology_data:
|
||||
evaluator = OntologyEvaluator()
|
||||
ontology_eval_result = evaluator.evaluate_ontology(
|
||||
ontology_data[0].data,
|
||||
competency_questions=[
|
||||
"What capability gaps are revealed for a mission thread?",
|
||||
"Which systems provide required capabilities?",
|
||||
"What evidence and provenance support a gap decision?",
|
||||
"Which precedents and exceptions affected a decision?",
|
||||
],
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 3) Parsing (Semantica parse)
|
||||
# -------------------------------------------------------------------------
|
||||
# Parse PDFs through parse methods, with parser-class fallback.
|
||||
pdf_docs = []
|
||||
pdf_parser = PDFParser()
|
||||
doc_parser_preview = {}
|
||||
docling_preview = {"docling_available": bool(DOCLING_AVAILABLE)}
|
||||
|
||||
for pdf_path in sorted(data_dir.glob("*.pdf")):
|
||||
try:
|
||||
# Primary parse path: module-level `parse_pdf`.
|
||||
parsed = parse_pdf(pdf_path, method="default", pages=list(range(0, 12)))
|
||||
if not isinstance(parsed, dict):
|
||||
# Fallback path: direct parser class call.
|
||||
parsed = pdf_parser.parse(pdf_path, pages=list(range(0, 12)))
|
||||
text = parsed.get("full_text", parsed.get("text", ""))
|
||||
if text:
|
||||
pdf_docs.append(
|
||||
{
|
||||
"doc_id": pdf_path.stem,
|
||||
"source": str(pdf_path),
|
||||
"text": text[:50000],
|
||||
"metadata": parsed.get("metadata", {}),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"PDF parse failed for {pdf_path.name}: {exc}")
|
||||
|
||||
if pdf_docs:
|
||||
sample_pdf = Path(pdf_docs[0]["source"])
|
||||
try:
|
||||
# Generic multi-format parse via `parse_document`.
|
||||
parsed_doc = parse_document(sample_pdf, method="default")
|
||||
if not isinstance(parsed_doc, dict):
|
||||
parsed_doc = DocumentParser().parse_document(sample_pdf)
|
||||
doc_parser_preview = {
|
||||
"source": sample_pdf.name,
|
||||
"keys": list(parsed_doc.keys())[:10],
|
||||
"text_chars": len(parsed_doc.get("full_text", parsed_doc.get("text", "")) or ""),
|
||||
}
|
||||
except Exception as exc:
|
||||
doc_parser_preview = {"source": sample_pdf.name, "error": str(exc)}
|
||||
|
||||
if docling_preview["docling_available"]:
|
||||
try:
|
||||
# Optional DoclingParser path when dependency is available.
|
||||
docling_parser = DoclingParser(export_format="markdown")
|
||||
dres = docling_parser.parse(sample_pdf)
|
||||
docling_preview["keys"] = list(dres.keys())[:10]
|
||||
docling_preview["text_chars"] = len(dres.get("full_text", dres.get("text", "")) or "")
|
||||
except Exception as exc:
|
||||
docling_preview["error"] = str(exc)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 4) Corpus assembly (input for split/normalize/extract)
|
||||
# -------------------------------------------------------------------------
|
||||
# Unify parsed PDFs, web documents, and ontology JSON snapshots.
|
||||
corpus = (
|
||||
[
|
||||
{"doc_id": d["doc_id"], "source": d["source"], "text": d["text"]}
|
||||
for d in pdf_docs
|
||||
]
|
||||
+ [
|
||||
{
|
||||
"doc_id": f"web_{i}",
|
||||
"source": getattr(w, "url", f"web_source_{i}"),
|
||||
"text": (getattr(w, "content", str(w)) or "")[:30000],
|
||||
}
|
||||
for i, w in enumerate(web_contents)
|
||||
]
|
||||
+ [
|
||||
{
|
||||
"doc_id": Path(od.source_path).stem,
|
||||
"source": od.source_path,
|
||||
"text": json.dumps(od.data, ensure_ascii=True)[:40000],
|
||||
}
|
||||
for od in ontology_data
|
||||
]
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 5) Split + pipeline declaration (Semantica split + pipeline)
|
||||
# -------------------------------------------------------------------------
|
||||
# Batch splitting through `TextSplitter.split_batch`.
|
||||
splitter = TextSplitter(method="recursive", chunk_size=1800, chunk_overlap=250)
|
||||
chunks_by_doc = splitter.split_batch([doc.get("text", "") for doc in corpus])
|
||||
chunked_docs = []
|
||||
for doc, chunks in zip(corpus, chunks_by_doc):
|
||||
for idx, chunk in enumerate(chunks or []):
|
||||
chunked_docs.append(
|
||||
{
|
||||
"doc_id": f"{doc['doc_id']}::chunk_{idx}",
|
||||
"source": doc["source"],
|
||||
"text": chunk.text if hasattr(chunk, "text") else str(chunk),
|
||||
"parent_doc_id": doc["doc_id"],
|
||||
}
|
||||
)
|
||||
extraction_corpus = chunked_docs if chunked_docs else corpus
|
||||
|
||||
# Logical orchestration path declared with Semantica PipelineBuilder.
|
||||
pipeline = (
|
||||
PipelineBuilder()
|
||||
.add_step("ingest_sources", "ingest", sources=len(corpus))
|
||||
.add_step("chunk_context", "split", method="recursive")
|
||||
.add_step("semantic_extract", "extract", entity_relation_event_triplet=True)
|
||||
.add_step("build_context_graph", "context_graph")
|
||||
.add_step("policy_and_trace", "decision_trace_capture")
|
||||
.add_step("export_and_observe", "export_observability")
|
||||
.connect_steps("ingest_sources", "chunk_context")
|
||||
.connect_steps("chunk_context", "semantic_extract")
|
||||
.connect_steps("semantic_extract", "build_context_graph")
|
||||
.connect_steps("build_context_graph", "policy_and_trace")
|
||||
.connect_steps("policy_and_trace", "export_and_observe")
|
||||
.build(name="capability_gap_orchestration_path")
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 6) Normalization (Semantica normalize)
|
||||
# -------------------------------------------------------------------------
|
||||
# Apply clean -> normalize -> detect_language -> handle_encoding.
|
||||
normalized_extraction_corpus = []
|
||||
for item in extraction_corpus:
|
||||
cleaned = clean_text(item.get("text", ""), method="default")
|
||||
normalized_text = normalize_text(cleaned, method="default") if cleaned else ""
|
||||
lang = detect_language(normalized_text, method="default") if normalized_text else "en"
|
||||
_ = handle_encoding(normalized_text, method="default") if normalized_text else normalized_text
|
||||
normalized_extraction_corpus.append({**item, "text": normalized_text, "language": lang})
|
||||
extraction_corpus = normalized_extraction_corpus
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 7) Semantic extraction (Semantica semantic_extract)
|
||||
# -------------------------------------------------------------------------
|
||||
# Initialize Semantica extractors for entities, relations, events, triplets.
|
||||
ner = NamedEntityRecognizer(method="pattern", confidence_threshold=0.2)
|
||||
rel_extractor = RelationExtractor(method="pattern", confidence_threshold=0.2)
|
||||
evt_detector = EventDetector()
|
||||
coref = CoreferenceResolver()
|
||||
triplet_extractor = TripletExtractor(method="pattern", include_provenance=True)
|
||||
analyzer = SemanticAnalyzer()
|
||||
net_extractor = SemanticNetworkExtractor()
|
||||
validator = ExtractionValidator()
|
||||
|
||||
# Resolve pronouns/coreferences before extraction to improve link quality.
|
||||
texts = [item.get("text", "") for item in extraction_corpus if item.get("text")]
|
||||
resolved_texts = [coref.resolve(t) for t in texts]
|
||||
|
||||
# Batch-first extraction for entities/triplets; per-text for relations/events.
|
||||
entities_batch = ner.process_batch(resolved_texts)
|
||||
triplets_batch = triplet_extractor.process_batch(resolved_texts)
|
||||
relations_batch = [rel_extractor.extract_relations(t, entities=e) for t, e in zip(resolved_texts, entities_batch)]
|
||||
events_batch = [evt_detector.detect_events(t) for t in resolved_texts]
|
||||
|
||||
all_entities = [e for batch in entities_batch for e in batch]
|
||||
all_relationships = [r for batch in relations_batch for r in batch]
|
||||
all_events = [ev for batch in events_batch for ev in batch]
|
||||
all_triplets = [tr for batch in triplets_batch for tr in batch]
|
||||
|
||||
# Validation step keeps extraction quality checks explicit.
|
||||
_ = validator.validate_entities(all_entities)
|
||||
_ = validator.validate_relations(all_relationships)
|
||||
|
||||
semantic_networks = [
|
||||
{
|
||||
"doc_id": extraction_corpus[i].get("doc_id", f"doc_{i}"),
|
||||
"analysis": analyzer.analyze(resolved_texts[i]),
|
||||
"network": net_extractor.extract(resolved_texts[i], entities=entities_batch[i], relations=relations_batch[i]),
|
||||
}
|
||||
for i in range(min(len(resolved_texts), len(extraction_corpus)))
|
||||
]
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 8) Quality controls + KG analytics (Semantica kg + conflicts)
|
||||
# -------------------------------------------------------------------------
|
||||
# Entity resolution for duplicate mentions.
|
||||
resolver = EntityResolver(strategy="fuzzy")
|
||||
entity_dicts = [
|
||||
{
|
||||
"id": str(getattr(e, "id", getattr(e, "text", "unknown"))),
|
||||
"name": str(getattr(e, "text", getattr(e, "id", "unknown"))),
|
||||
"type": str(getattr(e, "label", getattr(e, "type", "entity"))),
|
||||
"metadata": getattr(e, "metadata", {}) or {},
|
||||
}
|
||||
for e in all_entities
|
||||
]
|
||||
resolved_entities = resolver.resolve_entities(entity_dicts[:200]) if entity_dicts else []
|
||||
|
||||
# Conflict detection and resolution on contradictory numeric evidence.
|
||||
conflicts = detect_conflicts(
|
||||
[
|
||||
{"id": "System_GroundRadarLayer", "coveragePercent": "42", "type": "system"},
|
||||
{"id": "System_GroundRadarLayer", "coveragePercent": "58", "type": "system"},
|
||||
],
|
||||
method="value",
|
||||
property_name="coveragePercent",
|
||||
)
|
||||
resolved_conflicts = resolve_conflicts(conflicts, method=voting) if conflicts else []
|
||||
|
||||
# Build the knowledge graph from extracted entities/relationships.
|
||||
builder = GraphBuilder(merge_entities=True, resolve_conflicts=True)
|
||||
kg = builder.build([{"entities": all_entities, "relationships": all_relationships}], extract=False)
|
||||
|
||||
analyzer_kg = GraphAnalyzer()
|
||||
kg_analysis = analyzer_kg.analyze_graph(kg)
|
||||
|
||||
# Graph analytics modules: centrality, communities, connectivity, similarity.
|
||||
centrality_calc = CentralityCalculator()
|
||||
community_detector = CommunityDetector()
|
||||
connectivity_analyzer = ConnectivityAnalyzer()
|
||||
similarity_calc = SimilarityCalculator(method="cosine")
|
||||
link_predictor = LinkPredictor()
|
||||
node_embed_status = {}
|
||||
try:
|
||||
_ = NodeEmbedder(method="node2vec", embedding_dimension=32, walk_length=20, num_walks=5)
|
||||
node_embed_status["node2vec_ready"] = True
|
||||
except Exception as exc:
|
||||
node_embed_status = {"node2vec_ready": False, "reason": str(exc)}
|
||||
|
||||
extended_kg_analytics = {
|
||||
"centrality": centrality_calc.calculate_all_centrality(kg),
|
||||
"communities": community_detector.detect_communities(kg, algorithm="louvain"),
|
||||
"connectivity": connectivity_analyzer.analyze_connectivity(kg),
|
||||
"sample_cosine_similarity": similarity_calc.cosine_similarity([1.0, 0.0, 1.0], [0.8, 0.2, 0.9]),
|
||||
"predicted_links": link_predictor.predict_links(kg, top_k=5),
|
||||
"node_embedding_status": node_embed_status,
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 9) Context graph + decision traces (Semantica context)
|
||||
# -------------------------------------------------------------------------
|
||||
# Domain skeleton nodes/edges for scenario -> mission -> event -> gap chain.
|
||||
context_graph = ContextGraph(advanced_analytics=True, centrality_analysis=True, community_detection=True)
|
||||
context_graph.add_nodes(
|
||||
[
|
||||
{"id": "Scenario_FutureA2AD_2028", "type": "scenario", "properties": {"content": "Future A2/AD escalation scenario"}},
|
||||
{"id": "MissionThread_ForceProtection", "type": "mission_thread", "properties": {"content": "Protect forward operating assets under drone saturation"}},
|
||||
{"id": "Event_LowAltitudeSwarmIncursions", "type": "event", "properties": {"content": "Repeated low-altitude swarm incursions"}},
|
||||
{"id": "System_GroundRadarLayer", "type": "system", "properties": {"content": "Ground radar surveillance layer"}},
|
||||
{"id": "Capability_LowAltitudeDetection", "type": "capability", "properties": {"content": "Low altitude detection capability"}},
|
||||
{"id": "Outcome_MissionRiskIncrease", "type": "outcome", "properties": {"content": "Rising mission risk and delayed response"}},
|
||||
{"id": "Gap_LowAltitudeDetectionCoverage", "type": "capability_gap", "properties": {"content": "Insufficient low-altitude detection coverage"}},
|
||||
]
|
||||
)
|
||||
context_graph.add_edges(
|
||||
[
|
||||
{"source_id": "Scenario_FutureA2AD_2028", "target_id": "MissionThread_ForceProtection", "type": "has_mission_thread"},
|
||||
{"source_id": "MissionThread_ForceProtection", "target_id": "Event_LowAltitudeSwarmIncursions", "type": "includes_event"},
|
||||
{"source_id": "Event_LowAltitudeSwarmIncursions", "target_id": "System_GroundRadarLayer", "type": "stresses_system"},
|
||||
{"source_id": "System_GroundRadarLayer", "target_id": "Capability_LowAltitudeDetection", "type": "provides_capability"},
|
||||
{"source_id": "Capability_LowAltitudeDetection", "target_id": "Outcome_MissionRiskIncrease", "type": "affects_outcome"},
|
||||
{"source_id": "MissionThread_ForceProtection", "target_id": "Gap_LowAltitudeDetectionCoverage", "type": "reveals_gap"},
|
||||
]
|
||||
)
|
||||
|
||||
# AgentContext binds vector retrieval + context graph decision tracking.
|
||||
vector_store = VectorStore(backend="inmemory", dimension=384)
|
||||
agent_context = AgentContext(
|
||||
vector_store=vector_store,
|
||||
knowledge_graph=context_graph,
|
||||
decision_tracking=True,
|
||||
advanced_analytics=True,
|
||||
kg_algorithms=True,
|
||||
vector_store_features=True,
|
||||
graph_expansion=True,
|
||||
max_expansion_hops=3,
|
||||
)
|
||||
|
||||
_ = agent_context.store(
|
||||
[{"content": c["text"][:2500], "metadata": {"source": c["source"], "doc_id": c["doc_id"]}} for c in corpus],
|
||||
extract_entities=False,
|
||||
extract_relationships=False,
|
||||
)
|
||||
|
||||
# Example decision record for capability-gap assessment.
|
||||
decision_a = agent_context.record_decision(
|
||||
category="capability_gap_assessment",
|
||||
scenario="Future A2/AD mission thread with low-altitude swarm pressure",
|
||||
reasoning="Mission requires persistent low-altitude detection, but current radar layer indicates limited valley and urban coverage.",
|
||||
outcome="gap_identified_low_altitude_detection",
|
||||
confidence=0.93,
|
||||
entities=["MissionThread_ForceProtection", "Capability_LowAltitudeDetection", "Gap_LowAltitudeDetectionCoverage"],
|
||||
)
|
||||
|
||||
# Explicit policy-bound trace decision object.
|
||||
trace_decision = Decision(
|
||||
decision_id="",
|
||||
category="capability_gap_assessment",
|
||||
scenario="Coverage threshold breach during swarm-pressure mission thread",
|
||||
reasoning="Below-threshold low-altitude detection coverage with repeated threat ingress; escalation required.",
|
||||
outcome="escalate_for_exception",
|
||||
confidence=0.89,
|
||||
timestamp=datetime.now(),
|
||||
decision_maker="joint_ops_agent",
|
||||
metadata={"policy_version": "3.2"},
|
||||
)
|
||||
|
||||
# Policy and compliance checks against decision trace.
|
||||
policy_engine = PolicyEngine(context_graph)
|
||||
policy = Policy(
|
||||
policy_id="POL-CAPGAP-3.2",
|
||||
name="Capability Gap Escalation Policy",
|
||||
description="Escalate and require approval when mission-critical capability coverage is below threshold.",
|
||||
rules={
|
||||
"min_confidence": 0.8,
|
||||
"required_categories": ["capability_gap_assessment", "capability_gap_mitigation"],
|
||||
"allowed_outcomes": [
|
||||
"gap_identified_low_altitude_detection",
|
||||
"recommend_multilayer_sensor_fusion",
|
||||
"escalate_for_exception",
|
||||
],
|
||||
},
|
||||
category="capability_gap_assessment",
|
||||
version="3.2",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
metadata={"entities": ["MissionThread_ForceProtection", "System_GroundRadarLayer"]},
|
||||
)
|
||||
policy_engine.add_policy(policy)
|
||||
trace_decision_id = context_graph.record_decision(
|
||||
category=trace_decision.category,
|
||||
scenario=trace_decision.scenario,
|
||||
reasoning=trace_decision.reasoning,
|
||||
outcome=trace_decision.outcome,
|
||||
confidence=trace_decision.confidence,
|
||||
entities=["MissionThread_ForceProtection", "Gap_LowAltitudeDetectionCoverage"],
|
||||
decision_maker=trace_decision.decision_maker,
|
||||
metadata={
|
||||
"policy_version": "3.2",
|
||||
"cross_system_context": {"crm": "critical_account", "zendesk": "open_escalation", "pagerduty": "sev1_incidents"},
|
||||
},
|
||||
)
|
||||
|
||||
# Retrieve precedents and run multi-hop traversal in the context graph.
|
||||
_ = policy_engine.check_compliance(trace_decision, "POL-CAPGAP-3.2")
|
||||
_ = agent_context.find_precedents(
|
||||
scenario="Low-altitude detection shortfall under swarm pressure",
|
||||
category="capability_gap_assessment",
|
||||
limit=5,
|
||||
use_hybrid_search=True,
|
||||
)
|
||||
_ = context_graph.analyze_decision_impact(trace_decision_id)
|
||||
_ = multi_hop_query(
|
||||
context_graph,
|
||||
start_entity="Scenario_FutureA2AD_2028",
|
||||
query="Trace mission-thread to capability-gap path",
|
||||
max_hops=3,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 10) Rule-based reasoning (Semantica reasoning)
|
||||
# -------------------------------------------------------------------------
|
||||
reasoner = Reasoner()
|
||||
reasoner.add_rule("IF MissionRequires(?m, LowAltitudeDetection) AND CoverageStatus(?m, Insufficient) THEN CapabilityGap(?m, LowAltitudeDetectionGap)")
|
||||
reasoner.add_rule("IF CapabilityGap(?m, LowAltitudeDetectionGap) AND ThreatLevel(?m, High) THEN OutcomeRisk(?m, Elevated)")
|
||||
reasoner.add_fact("MissionRequires(MissionThread_ForceProtection, LowAltitudeDetection)")
|
||||
reasoner.add_fact("CoverageStatus(MissionThread_ForceProtection, Insufficient)")
|
||||
reasoner.add_fact("ThreatLevel(MissionThread_ForceProtection, High)")
|
||||
inferred = reasoner.forward_chain()
|
||||
explanation_text = ""
|
||||
if inferred:
|
||||
explanation = ExplanationGenerator().generate_explanation(inferred[-1])
|
||||
explanation_text = explanation.natural_language
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 11) Versioning + provenance (Semantica change_management + provenance)
|
||||
# -------------------------------------------------------------------------
|
||||
# Version policies/ontology structure and store lineage records.
|
||||
version_manager = VersionManager(base_uri="https://example.org/mcg")
|
||||
_ = version_manager.create_version(
|
||||
"3.1",
|
||||
ontology={"uri": "https://example.org/mcg", "classes": [], "properties": []},
|
||||
changes=["Initial capability-gap decision policy baseline"],
|
||||
metadata={"structure": {"classes": ["Scenario", "MissionThread", "CapabilityGap"], "properties": ["revealsGap"]}},
|
||||
)
|
||||
_ = version_manager.create_version(
|
||||
"3.2",
|
||||
ontology={"uri": "https://example.org/mcg", "classes": [], "properties": []},
|
||||
changes=["Added explicit policy exception and approval-chain trace constructs"],
|
||||
metadata={
|
||||
"structure": {
|
||||
"classes": ["Scenario", "MissionThread", "CapabilityGap", "PolicyException", "ApprovalChain"],
|
||||
"properties": ["revealsGap", "has_exception", "approved_by_chain"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
prov = ProvenanceManager(storage_path=str(output_dir / "capability_gap_provenance.db"))
|
||||
for c in corpus:
|
||||
prov.track_entity(entity_id=f"source::{c['doc_id']}", source=c["source"], metadata={"document_type": "corpus_source"})
|
||||
for i, rel in enumerate(all_relationships[:120]):
|
||||
prov.track_relationship(
|
||||
relationship_id=f"rel::{i}",
|
||||
source=(getattr(rel, "metadata", {}) or {}).get("source_doc", "unknown_source"),
|
||||
metadata={"relation_type": str(getattr(rel, "predicate", getattr(rel, "type", "related_to")))},
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 12) Export + visualization (Semantica export + visualization)
|
||||
# -------------------------------------------------------------------------
|
||||
# Export graph/context in multiple formats for downstream tools.
|
||||
export_json(kg, output_dir / "capability_gap_kg.json", format="json")
|
||||
export_json(context_graph.to_dict(), output_dir / "capability_gap_context_graph.json", format="json")
|
||||
export_graph(context_graph.to_dict(), output_dir / "capability_gap_context_graph.graphml", format="graphml")
|
||||
export_rdf(kg, output_dir / "capability_gap_kg.ttl", format="turtle")
|
||||
export_csv({"entities": kg.get("entities", []), "relationships": kg.get("relationships", [])}, output_dir / "capability_gap_kg")
|
||||
export_yaml(context_graph.to_dict(), output_dir / "capability_gap_context_graph.yaml")
|
||||
export_lpg(kg, output_dir / "capability_gap_kg.cypher", method="cypher")
|
||||
|
||||
report_data = {
|
||||
"title": "Military Capability Gap Analysis - End-to-End Report",
|
||||
"summary": {
|
||||
"corpus_items": len(corpus),
|
||||
"extraction_items": len(extraction_corpus),
|
||||
"entities": len(all_entities),
|
||||
"relationships": len(all_relationships),
|
||||
"decisions": context_graph.get_decision_summary().get("total_decisions", 0),
|
||||
},
|
||||
"metrics": {
|
||||
"kg_entities": len(kg.get("entities", [])),
|
||||
"kg_relationships": len(kg.get("relationships", [])),
|
||||
"context_nodes": context_graph.stats().get("node_count", 0),
|
||||
"context_edges": context_graph.stats().get("edge_count", 0),
|
||||
},
|
||||
"analysis": {"kg_analysis": kg_analysis},
|
||||
}
|
||||
ReportGenerator(format="markdown", include_charts=False).generate_report(
|
||||
report_data,
|
||||
output_dir / "capability_gap_analysis_report.md",
|
||||
format="markdown",
|
||||
)
|
||||
|
||||
# Optional network HTML visualization.
|
||||
try:
|
||||
KGVisualizer(layout="force", color_scheme="default").visualize_network(
|
||||
kg,
|
||||
output="html",
|
||||
file_path=output_dir / "capability_gap_kg_network.html",
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"Visualization skipped: {exc}")
|
||||
|
||||
# Final run summary for quick validation.
|
||||
summary = {
|
||||
"use_case_dir": str(use_case_dir),
|
||||
"output_dir": str(output_dir),
|
||||
"files_ingested": len(file_objects),
|
||||
"web_docs_ingested": len(web_contents),
|
||||
"ontologies_ingested": len(ontology_data),
|
||||
"ontology_details": ontology_details,
|
||||
"ontology_eval": {
|
||||
"coverage_score": getattr(ontology_eval_result, "coverage_score", None),
|
||||
"completeness_score": getattr(ontology_eval_result, "completeness_score", None),
|
||||
},
|
||||
"doc_parser_preview": doc_parser_preview,
|
||||
"docling_preview": docling_preview,
|
||||
"pipeline": pipeline.name,
|
||||
"entities_extracted": len(all_entities),
|
||||
"relationships_extracted": len(all_relationships),
|
||||
"events_detected": len(all_events),
|
||||
"triplets_extracted": len(all_triplets),
|
||||
"semantic_networks": len(semantic_networks),
|
||||
"resolved_entities": len(resolved_entities),
|
||||
"conflicts_detected": len(conflicts),
|
||||
"conflicts_resolved": len(resolved_conflicts),
|
||||
"reasoning_inferred_rules": [r.conclusion for r in inferred],
|
||||
"reasoning_explanation": explanation_text,
|
||||
"extended_kg_analytics_keys": list(extended_kg_analytics.keys()),
|
||||
"provenance_stats": prov.get_statistics(),
|
||||
"decision_example": decision_a,
|
||||
}
|
||||
print(json.dumps(summary, indent=2, ensure_ascii=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
Apache Parquet Exporter - Example Usage
|
||||
|
||||
This script demonstrates how to use the ParquetExporter to export
|
||||
knowledge graphs, entities, and relationships to Apache Parquet format.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from semantica.export import ParquetExporter, export_parquet
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("Apache Parquet Exporter - Example Usage")
|
||||
print("=" * 70)
|
||||
|
||||
# Create a temporary directory for outputs
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
print(f"\n📁 Output directory: {temp_dir}\n")
|
||||
|
||||
# Sample data
|
||||
entities = [
|
||||
{
|
||||
"id": "e1",
|
||||
"text": "Alice",
|
||||
"type": "Person",
|
||||
"confidence": 0.95,
|
||||
"start": 0,
|
||||
"end": 5,
|
||||
"metadata": {"age": 30, "city": "New York"},
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"text": "Acme Corp",
|
||||
"type": "Organization",
|
||||
"confidence": 0.88,
|
||||
"start": 10,
|
||||
"end": 19,
|
||||
"metadata": {"location": "NY", "employees": 100},
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"text": "Bob",
|
||||
"type": "Person",
|
||||
"confidence": 0.92,
|
||||
"metadata": {"age": 35, "department": "Engineering"},
|
||||
},
|
||||
]
|
||||
|
||||
relationships = [
|
||||
{
|
||||
"id": "r1",
|
||||
"source_id": "e1",
|
||||
"target_id": "e2",
|
||||
"type": "WORKS_FOR",
|
||||
"confidence": 0.90,
|
||||
"metadata": {"role": "Engineer", "since": 2020},
|
||||
},
|
||||
{
|
||||
"id": "r2",
|
||||
"source_id": "e3",
|
||||
"target_id": "e2",
|
||||
"type": "WORKS_FOR",
|
||||
"confidence": 0.85,
|
||||
"metadata": {"role": "Manager", "since": 2018},
|
||||
},
|
||||
]
|
||||
|
||||
knowledge_graph = {
|
||||
"entities": entities,
|
||||
"relationships": relationships,
|
||||
"metadata": {"version": "1.0", "created": "2024-01-01"},
|
||||
}
|
||||
|
||||
# Example 1: Export entities using ParquetExporter class
|
||||
print("Example 1: Export entities to Parquet")
|
||||
print("-" * 70)
|
||||
exporter = ParquetExporter(compression="snappy")
|
||||
entities_path = temp_dir / "entities.parquet"
|
||||
exporter.export_entities(entities, entities_path)
|
||||
print(f"✓ Entities exported to: {entities_path}")
|
||||
print(f" File size: {entities_path.stat().st_size} bytes\n")
|
||||
|
||||
# Example 2: Export relationships
|
||||
print("Example 2: Export relationships to Parquet")
|
||||
print("-" * 70)
|
||||
rels_path = temp_dir / "relationships.parquet"
|
||||
exporter.export_relationships(relationships, rels_path)
|
||||
print(f"✓ Relationships exported to: {rels_path}")
|
||||
print(f" File size: {rels_path.stat().st_size} bytes\n")
|
||||
|
||||
# Example 3: Export complete knowledge graph
|
||||
print("Example 3: Export knowledge graph to multiple Parquet files")
|
||||
print("-" * 70)
|
||||
kg_base_path = temp_dir / "knowledge_graph"
|
||||
exporter.export_knowledge_graph(knowledge_graph, kg_base_path)
|
||||
kg_entities = temp_dir / "knowledge_graph_entities.parquet"
|
||||
kg_rels = temp_dir / "knowledge_graph_relationships.parquet"
|
||||
print("✓ Knowledge graph exported to:")
|
||||
print(f" - {kg_entities} ({kg_entities.stat().st_size} bytes)")
|
||||
print(f" - {kg_rels} ({kg_rels.stat().st_size} bytes)\n")
|
||||
|
||||
# Example 4: Using convenience function
|
||||
print("Example 4: Using export_parquet convenience function")
|
||||
print("-" * 70)
|
||||
conv_path = temp_dir / "convenience_export.parquet"
|
||||
export_parquet(entities, conv_path, compression="gzip")
|
||||
print(f"✓ Exported using convenience function: {conv_path}")
|
||||
print(f" File size: {conv_path.stat().st_size} bytes\n")
|
||||
|
||||
# Example 5: Different compression codecs
|
||||
print("Example 5: Compare compression codecs")
|
||||
print("-" * 70)
|
||||
|
||||
# Create larger dataset for meaningful comparison
|
||||
large_entities = entities * 50
|
||||
|
||||
compression_codecs = ["snappy", "gzip", "brotli", "zstd", "lz4", "none"]
|
||||
sizes = {}
|
||||
|
||||
for codec in compression_codecs:
|
||||
codec_exporter = ParquetExporter(compression=codec)
|
||||
codec_path = temp_dir / f"entities_{codec}.parquet"
|
||||
codec_exporter.export_entities(large_entities, codec_path)
|
||||
sizes[codec] = codec_path.stat().st_size
|
||||
print(f" {codec:8} - {sizes[codec]:,} bytes")
|
||||
|
||||
print()
|
||||
|
||||
# Example 6: Load Parquet with pandas (if available)
|
||||
print("Example 6: Loading Parquet files with pandas")
|
||||
print("-" * 70)
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_parquet(entities_path)
|
||||
print("✓ Loaded entities as pandas DataFrame")
|
||||
print(f" Shape: {df.shape}")
|
||||
print(f" Columns: {list(df.columns)}")
|
||||
print("\nFirst few rows:")
|
||||
print(df.head())
|
||||
print()
|
||||
|
||||
except ImportError:
|
||||
print("⚠ pandas not installed - skipping pandas example\n")
|
||||
|
||||
# Example 7: Load Parquet with pyarrow
|
||||
print("Example 7: Loading Parquet files with pyarrow")
|
||||
print("-" * 70)
|
||||
try:
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
table = pq.read_table(entities_path)
|
||||
print("✓ Loaded entities as Arrow Table")
|
||||
print(f" Rows: {table.num_rows}")
|
||||
print(f" Columns: {table.num_columns}")
|
||||
print(" Schema:")
|
||||
for i, field in enumerate(table.schema):
|
||||
print(f" - {field.name}: {field.type}")
|
||||
print()
|
||||
|
||||
except ImportError:
|
||||
print("⚠ pyarrow not installed - skipping pyarrow example\n")
|
||||
|
||||
# Example 8: Schema validation
|
||||
print("Example 8: Explicit schema validation")
|
||||
print("-" * 70)
|
||||
try:
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
# Read parquet file and verify schema
|
||||
table = pq.read_table(entities_path)
|
||||
|
||||
print("✓ Schema validation:")
|
||||
print(f" - ID column type: {table.schema.field('id').type}")
|
||||
print(f" - Text column type: {table.schema.field('text').type}")
|
||||
print(f" - Confidence column type: {table.schema.field('confidence').type}")
|
||||
print(f" - Metadata column type: {table.schema.field('metadata').type}")
|
||||
print()
|
||||
|
||||
# Verify metadata structure
|
||||
metadata_field = table.schema.field("metadata")
|
||||
print(" Metadata structure:")
|
||||
if hasattr(metadata_field.type, "num_fields"):
|
||||
for i in range(metadata_field.type.num_fields):
|
||||
subfield = metadata_field.type.field(i)
|
||||
print(f" - {subfield.name}: {subfield.type}")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠ Schema validation error: {e}\n")
|
||||
|
||||
# Summary
|
||||
print("=" * 70)
|
||||
print("Summary")
|
||||
print("=" * 70)
|
||||
print("✓ All examples completed successfully")
|
||||
print(f"✓ Output directory: {temp_dir}")
|
||||
print(f"✓ Files created: {len(list(temp_dir.glob('*.parquet')))}")
|
||||
print("\nKey Features:")
|
||||
print(" - Columnar storage optimized for analytics")
|
||||
print(" - Multiple compression options (snappy, gzip, brotli, zstd, lz4)")
|
||||
print(" - Compatible with pandas, Spark, Snowflake, BigQuery, Databricks")
|
||||
print(" - Explicit schemas for type safety")
|
||||
print(" - Structured metadata handling")
|
||||
print("\nFor more information, see the Semantica documentation.")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,197 @@
|
||||
# Semantica Knowledge Explorer
|
||||
|
||||
A real-time visual interface for exploring knowledge graphs, decision intelligence, entity resolution, ontologies, and graph analytics built on top of the [Semantica](https://github.com/Hawksight-AI/semantica) library.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|---|---|
|
||||
| Node.js | 18.x or higher (20.x recommended) |
|
||||
| npm | 9.x or higher |
|
||||
| Python | 3.8+ |
|
||||
| Semantica backend | running on `http://127.0.0.1:8000` |
|
||||
|
||||
Check your versions:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
python --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Local Development)
|
||||
|
||||
### 1. Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Hawksight-AI/semantica.git
|
||||
cd semantica
|
||||
```
|
||||
|
||||
### 2. Install the Semantica Python package
|
||||
|
||||
```bash
|
||||
pip install semantica
|
||||
```
|
||||
|
||||
Or install from source if you have the repo:
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### 3. Start the Semantica backend
|
||||
|
||||
The Explorer proxies all `/api` and `/ws` requests to `http://127.0.0.1:8000`. The backend must be running before you open the UI.
|
||||
|
||||
```bash
|
||||
# From the repo root
|
||||
python -m semantica.server
|
||||
```
|
||||
|
||||
The backend starts on port **8000** by default. Keep this terminal open.
|
||||
|
||||
### 4. Install frontend dependencies
|
||||
|
||||
Open a second terminal:
|
||||
|
||||
```bash
|
||||
cd explorer
|
||||
npm install
|
||||
```
|
||||
|
||||
> **Note:** This project uses Vite 5 and requires **Node 18+**. If you are on Node 16 or earlier, upgrade first.
|
||||
|
||||
### 5. Start the dev server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Vite starts on **http://localhost:5173** by default. Open that URL in your browser.
|
||||
|
||||
---
|
||||
|
||||
## What you should see
|
||||
|
||||
The Explorer opens with a persistent left sidebar and six workspace tabs:
|
||||
|
||||
| Tab | What it shows |
|
||||
|---|---|
|
||||
| **Knowledge Graph** | Interactive Sigma.js canvas — nodes, edges, zoom, ForceAtlas2 layout |
|
||||
| **Timeline** | Temporal event scrubber over the graph |
|
||||
| **Decisions** | Causal chain viewer with outcome badges and decision filter |
|
||||
| **Registry** | Live audit log of every graph mutation (add-node, add-edge, etc.) |
|
||||
| **Entity Resolution** | Duplicate detection and entity merge workflow |
|
||||
| **KG Overview** | Aggregate stats, community breakdown, centrality heatmap |
|
||||
| **Ontology** | SKOS/OWL vocabulary hierarchy and schema summary |
|
||||
|
||||
---
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
explorer/
|
||||
├── src/
|
||||
│ ├── App.tsx # Root layout, tab routing, workspace wiring
|
||||
│ ├── index.css # Global resets, fonts, keyframe animations
|
||||
│ ├── store/
|
||||
│ │ └── registryStore.ts # Pub/sub audit registry (no external state lib)
|
||||
│ └── workspaces/
|
||||
│ ├── GraphWorkspace/ # Sigma.js graph canvas + inspector panel
|
||||
│ ├── DecisionWorkspace/ # Causal flow diagram + decision list
|
||||
│ ├── TimelineWorkspace/ # vis-timeline temporal scrubber
|
||||
│ ├── ManageWorkspace/ # Registry, KG Overview, Ontology tabs
|
||||
│ └── EnrichWorkspace/ # Entity resolution tab
|
||||
├── index.html
|
||||
├── vite.config.ts # Dev proxy → 127.0.0.1:8000, build → ../semantica/static
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available scripts
|
||||
|
||||
Run these from inside the `explorer/` directory:
|
||||
|
||||
```bash
|
||||
# Start the dev server with hot module replacement
|
||||
npm run dev
|
||||
|
||||
# Type-check and build a production bundle into ../semantica/static
|
||||
npm run build
|
||||
|
||||
# Preview the production build locally
|
||||
npm run preview
|
||||
|
||||
# Run ESLint over all source files
|
||||
npm run lint
|
||||
|
||||
# Run the graph store multi-edge unit tests
|
||||
npm run test:graph-store
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API & WebSocket proxy
|
||||
|
||||
During development, Vite forwards requests automatically — no CORS configuration needed:
|
||||
|
||||
| Pattern | Forwarded to |
|
||||
|---|---|
|
||||
| `/api/*` | `http://127.0.0.1:8000/api/*` |
|
||||
| `/ws` | `ws://127.0.0.1:8000/ws` |
|
||||
|
||||
If you run the backend on a different port, update `server.proxy` in [vite.config.ts](vite.config.ts).
|
||||
|
||||
---
|
||||
|
||||
## Production build
|
||||
|
||||
```bash
|
||||
cd explorer
|
||||
npm run build
|
||||
```
|
||||
|
||||
The compiled assets are written to `../semantica/static/`. The Semantica Python server serves this folder automatically at its root URL — no separate web server needed.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Blank graph / no data loads**
|
||||
- Make sure the Semantica backend is running (`python -m semantica.server`) before opening the UI.
|
||||
- Check the browser console for failed `/api/graph` requests — the proxy target may need updating in `vite.config.ts`.
|
||||
|
||||
**`npm install` fails or hangs**
|
||||
- Ensure you are using **Node 18 or 20**. Node 16 and Vite 5 are incompatible.
|
||||
- Delete `node_modules/` and `package-lock.json`, then re-run `npm install`.
|
||||
|
||||
**Port 5173 already in use**
|
||||
- Vite will automatically try the next available port and print it in the terminal. Use that URL instead.
|
||||
|
||||
**WebSocket not connecting (real-time mutations not appearing)**
|
||||
- Confirm the backend exposes a `/ws` WebSocket endpoint.
|
||||
- Check browser DevTools → Network → WS tab for the connection status.
|
||||
|
||||
---
|
||||
|
||||
## Tech stack
|
||||
|
||||
- **React 19** + TypeScript (strict `noUnusedLocals`)
|
||||
- **Vite 5** with `babel-plugin-react-compiler`
|
||||
- **Sigma.js 3** + **Graphology** — graph rendering and in-memory graph store
|
||||
- **ForceAtlas2** — physics-based layout worker
|
||||
- **@tanstack/react-query** — data fetching for ontology and vocab tabs
|
||||
- **vis-timeline** — temporal event visualization
|
||||
- **lucide-react** — icon set
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
See the root [CONTRIBUTING.md](../CONTRIBUTING.md) and open issues on the main [Semantica repository](https://github.com/Hawksight-AI/semantica).
|
||||
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Semantica Knowledge Explorer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4710
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "semantica-knowledge-explorer",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test:graph-store": "node --test tests/graphStore.multi-edge.test.mjs",
|
||||
"test:graph-workspace": "node --import tsx --test tests/graphSceneState.display.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@sigma/edge-curve": "^3.1.0",
|
||||
"@sigma/node-border": "^3.0.0",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"graphology": "^0.26.0",
|
||||
"graphology-communities-louvain": "^2.0.2",
|
||||
"graphology-layout-forceatlas2": "^0.10.1",
|
||||
"graphology-metrics": "^2.4.0",
|
||||
"graphology-shortest-path": "^2.1.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"playwright": "^1.59.1",
|
||||
"react": "^19.2.4",
|
||||
"react-arborist": "^3.4.3",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-dropzone": "^15.0.0",
|
||||
"sigma": "^3.0.2",
|
||||
"vis-data": "^8.0.3",
|
||||
"vis-timeline": "^8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^5.4.0"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,71 @@
|
||||
/* ── Semantica Explorer — Global CSS Reset ── */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;600;700;800&display=swap');
|
||||
|
||||
*, *::before, *::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'IBM Plex Sans', 'Space Grotesk', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
/* Focus ring for accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid rgba(88, 166, 255, 0.6);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(88, 166, 255, 0.25);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(88, 166, 255, 0.45);
|
||||
}
|
||||
|
||||
/* Monospace font for code elements */
|
||||
code, pre, .mono {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
}
|
||||
|
||||
/* Selection highlight */
|
||||
::selection {
|
||||
background: rgba(88, 166, 255, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Spin animation for loaders */
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Skeleton pulse animation for loading placeholders */
|
||||
@keyframes skeleton-pulse {
|
||||
0%, 100% { opacity: 0.45; }
|
||||
50% { opacity: 0.85; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<App />,
|
||||
)
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export function pairRegistryKey(source: string, target: string): string;
|
||||
export function curveGroupForPair(source: string, target: string): string;
|
||||
@@ -0,0 +1,7 @@
|
||||
export function pairRegistryKey(source, target) {
|
||||
return JSON.stringify([source, target]);
|
||||
}
|
||||
|
||||
export function curveGroupForPair(source, target) {
|
||||
return JSON.stringify([source, target]);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import Graph from "graphology";
|
||||
import type {
|
||||
GraphArrowVisibilityPolicy,
|
||||
GraphBadgeKind,
|
||||
GraphEdgeVariant,
|
||||
GraphEntityShapeVariant,
|
||||
GraphLabelVisibilityPolicy,
|
||||
GraphNodeShapeVariant,
|
||||
} from "../workspaces/GraphWorkspace/graphTheme";
|
||||
import { curveGroupForPair, pairRegistryKey } from "./edgePairKeys.js";
|
||||
|
||||
|
||||
export const graph = new Graph({
|
||||
type: "directed",
|
||||
multi: true,
|
||||
allowSelfLoops: false
|
||||
});
|
||||
|
||||
|
||||
|
||||
export interface NodeAttributes {
|
||||
|
||||
label: string;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
color: string;
|
||||
baseColor?: string;
|
||||
mutedColor?: string;
|
||||
glowColor?: string;
|
||||
baseSize?: number;
|
||||
visualPriority?: number;
|
||||
labelPriority?: number;
|
||||
semanticGroup?: string;
|
||||
strokeColor?: string;
|
||||
borderColor?: string;
|
||||
borderSize?: number;
|
||||
nodeVariant?: GraphNodeShapeVariant;
|
||||
nodeShapeVariant?: GraphNodeShapeVariant;
|
||||
entityShape?: GraphEntityShapeVariant;
|
||||
badgeKind?: GraphBadgeKind;
|
||||
badgeCount?: number;
|
||||
ringColor?: string;
|
||||
haloColor?: string;
|
||||
labelVisibilityPolicy?: GraphLabelVisibilityPolicy;
|
||||
highlighted?: boolean;
|
||||
communityId?: string;
|
||||
isCommunityGroup?: boolean;
|
||||
memberCount?: number;
|
||||
anchorNodeId?: string | null;
|
||||
|
||||
nodeType: string;
|
||||
content: string;
|
||||
valid_from?: string | null;
|
||||
valid_until?: string | null;
|
||||
properties: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface EdgeAttributes {
|
||||
edgeId?: string;
|
||||
familyId?: string;
|
||||
sourceId?: string;
|
||||
targetId?: string;
|
||||
|
||||
size?: number;
|
||||
baseSize?: number;
|
||||
color?: string;
|
||||
baseColor?: string;
|
||||
mutedColor?: string;
|
||||
type?: string;
|
||||
curvature?: number;
|
||||
visualPriority?: number;
|
||||
edgeFamily?: "line" | "parallel" | "bidirectional" | "path";
|
||||
isBidirectional?: boolean;
|
||||
curveGroup?: string | null;
|
||||
edgeVariant?: GraphEdgeVariant;
|
||||
arrowVisibilityPolicy?: GraphArrowVisibilityPolicy;
|
||||
relationshipStrength?: number;
|
||||
isParallelPair?: boolean;
|
||||
parallelIndex?: number;
|
||||
parallelCount?: number;
|
||||
familySize?: number;
|
||||
rawEdgeIds?: string[];
|
||||
isAggregated?: boolean;
|
||||
aggregateCount?: number;
|
||||
dominantEdgeType?: string;
|
||||
representativeWeight?: number;
|
||||
bundleKind?: "parallel" | "bidirectional" | "community";
|
||||
|
||||
|
||||
edgeType: string;
|
||||
weight: number;
|
||||
properties: Record<string, any>;
|
||||
}
|
||||
|
||||
function normalizeParallelMetadataForPair(source: string, target: string): void {
|
||||
const edgeIds: string[] = [];
|
||||
graph.forEachDirectedEdge(source, target, (edgeId) => {
|
||||
edgeIds.push(String(edgeId));
|
||||
});
|
||||
|
||||
const pairCount = edgeIds.length;
|
||||
const familyCounts = new Map<string, number>();
|
||||
edgeIds.forEach((edgeId) => {
|
||||
const attrs = graph.getEdgeAttributes(edgeId) as EdgeAttributes;
|
||||
const familyId = String(attrs.familyId || edgeId);
|
||||
familyCounts.set(familyId, (familyCounts.get(familyId) ?? 0) + 1);
|
||||
});
|
||||
|
||||
edgeIds
|
||||
.sort((left, right) => {
|
||||
const leftAttrs = graph.getEdgeAttributes(left) as EdgeAttributes;
|
||||
const rightAttrs = graph.getEdgeAttributes(right) as EdgeAttributes;
|
||||
const priorityDelta = Number(rightAttrs.visualPriority ?? 0) - Number(leftAttrs.visualPriority ?? 0);
|
||||
if (priorityDelta !== 0) {
|
||||
return priorityDelta;
|
||||
}
|
||||
const weightDelta = Number(rightAttrs.weight ?? 0) - Number(leftAttrs.weight ?? 0);
|
||||
if (weightDelta !== 0) {
|
||||
return weightDelta;
|
||||
}
|
||||
return left.localeCompare(right);
|
||||
})
|
||||
.forEach((edgeId, index) => {
|
||||
const attrs = graph.getEdgeAttributes(edgeId) as EdgeAttributes;
|
||||
const familyId = String(attrs.familyId || edgeId);
|
||||
graph.mergeEdgeAttributes(edgeId, {
|
||||
edgeId,
|
||||
familyId,
|
||||
sourceId: source,
|
||||
targetId: target,
|
||||
isParallelPair: pairCount > 1,
|
||||
parallelIndex: index,
|
||||
parallelCount: pairCount,
|
||||
familySize: familyCounts.get(familyId) ?? 1,
|
||||
curveGroup: curveGroupForPair(source, target),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function batchMergeNodes(
|
||||
nodes: { id: string; attributes: NodeAttributes }[]
|
||||
): void {
|
||||
for (const { id, attributes } of nodes) {
|
||||
graph.mergeNode(id, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function batchMergeEdges(
|
||||
edges: { id: string; familyId?: string; source: string; target: string; attributes: EdgeAttributes }[]
|
||||
): void {
|
||||
const touchedPairs = new Map<string, { source: string; target: string }>();
|
||||
|
||||
for (const { id, familyId, source, target, attributes } of edges) {
|
||||
if (source === target) continue; // skip self-loops; graph was created with allowSelfLoops: false
|
||||
const edgeId = String(attributes.edgeId || id);
|
||||
const resolvedFamilyId = String(attributes.familyId || familyId || edgeId);
|
||||
|
||||
if (graph.hasNode(source) && graph.hasNode(target)) {
|
||||
graph.mergeDirectedEdgeWithKey(edgeId, source, target, {
|
||||
...attributes,
|
||||
edgeId,
|
||||
familyId: resolvedFamilyId,
|
||||
sourceId: source,
|
||||
targetId: target,
|
||||
});
|
||||
touchedPairs.set(pairRegistryKey(source, target), { source, target });
|
||||
}
|
||||
}
|
||||
|
||||
touchedPairs.forEach(({ source, target }) => {
|
||||
normalizeParallelMetadataForPair(source, target);
|
||||
});
|
||||
}
|
||||
|
||||
export function clearGraph(): void {
|
||||
graph.clear();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* src/store/registryStore.ts
|
||||
*
|
||||
* Lightweight client-side audit log for all KG / Ontology mutations.
|
||||
* No backend required — events are dispatched by each workspace after
|
||||
* a successful API call or WebSocket mutation.
|
||||
*
|
||||
* Any component can call logEvent() from anywhere (including non-React code).
|
||||
* React components subscribe via the useRegistry() hook.
|
||||
*/
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export type RegistryEntryOp =
|
||||
| "import"
|
||||
| "export"
|
||||
| "merge"
|
||||
| "add-node"
|
||||
| "add-edge"
|
||||
| "delete"
|
||||
| "infer"
|
||||
| "vocab-import";
|
||||
|
||||
export interface RegistryEntry {
|
||||
id: string;
|
||||
op: RegistryEntryOp;
|
||||
timestamp: Date;
|
||||
summary: string;
|
||||
detail?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type Listener = (entries: readonly RegistryEntry[]) => void;
|
||||
|
||||
let _entries: RegistryEntry[] = [];
|
||||
const _listeners = new Set<Listener>();
|
||||
const MAX_ENTRIES = 500;
|
||||
|
||||
function _notify(): void {
|
||||
_listeners.forEach((fn) => fn(_entries));
|
||||
}
|
||||
|
||||
export function logEvent(
|
||||
op: RegistryEntryOp,
|
||||
summary: string,
|
||||
detail?: Record<string, unknown>,
|
||||
): void {
|
||||
const entry: RegistryEntry = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
op,
|
||||
timestamp: new Date(),
|
||||
summary,
|
||||
detail,
|
||||
};
|
||||
_entries = [entry, ..._entries].slice(0, MAX_ENTRIES);
|
||||
_notify();
|
||||
}
|
||||
|
||||
export function clearRegistry(): void {
|
||||
_entries = [];
|
||||
_notify();
|
||||
}
|
||||
|
||||
export function getRegistryEntries(): readonly RegistryEntry[] {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
export function useRegistry(): readonly RegistryEntry[] {
|
||||
const [snapshot, setSnapshot] = useState<readonly RegistryEntry[]>(_entries);
|
||||
useEffect(() => {
|
||||
// Sync any events that arrived between render and subscribe
|
||||
setSnapshot(_entries);
|
||||
_listeners.add(setSnapshot);
|
||||
return () => {
|
||||
_listeners.delete(setSnapshot);
|
||||
};
|
||||
}, []);
|
||||
return snapshot;
|
||||
}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
declare module 'vis-timeline/standalone' {
|
||||
export class Timeline {
|
||||
constructor(container: HTMLElement, items: any, options?: any);
|
||||
on(event: string, callback: (properties: any) => void): void;
|
||||
destroy(): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vis-data/standalone' {
|
||||
export class DataSet<T = any> {
|
||||
constructor(data?: T[], options?: any);
|
||||
add(data: T | T[]): void;
|
||||
update(data: T | T[]): void;
|
||||
remove(id: string | number | (string | number)[]): void;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
.sem-workspace-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 18px;
|
||||
padding: 20px;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(103, 182, 255, 0.08), transparent 24%),
|
||||
linear-gradient(180deg, rgba(8, 17, 29, 0.94), rgba(3, 7, 14, 0.98));
|
||||
}
|
||||
|
||||
.sem-workspace-frame > :last-child {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.sem-workspace-hero {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
padding: 20px 22px;
|
||||
border-radius: 26px;
|
||||
border: 1px solid var(--panel-border);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(8, 18, 33, 0.88), rgba(11, 22, 38, 0.72)),
|
||||
radial-gradient(circle at top right, rgba(255, 179, 109, 0.08), transparent 28%);
|
||||
box-shadow: var(--shadow-strong), inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.sem-workspace-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-2);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sem-workspace-kicker::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, var(--accent-2), var(--warm));
|
||||
box-shadow: 0 0 14px rgba(158, 217, 255, 0.45);
|
||||
}
|
||||
|
||||
.sem-workspace-title {
|
||||
color: var(--text-1);
|
||||
font-size: 28px;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.05em;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sem-workspace-subtitle {
|
||||
margin-top: 8px;
|
||||
color: var(--text-2);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
.sem-workspace-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sem-grid-two {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.12fr) minmax(320px, 0.88fr);
|
||||
gap: 20px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sem-grid-split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sem-surface {
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--panel-border);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(9, 18, 32, 0.84), rgba(10, 18, 31, 0.72)),
|
||||
radial-gradient(circle at top, rgba(103, 182, 255, 0.05), transparent 34%);
|
||||
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.sem-surface--subtle {
|
||||
background: linear-gradient(180deg, rgba(9, 18, 32, 0.7), rgba(9, 16, 28, 0.52));
|
||||
}
|
||||
|
||||
.sem-surface--accent {
|
||||
border-color: var(--panel-border-strong);
|
||||
}
|
||||
|
||||
.sem-surface-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.sem-surface-body--tight {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.sem-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sem-section-eyebrow {
|
||||
color: var(--text-3);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sem-section-title {
|
||||
color: var(--text-1);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sem-section-copy {
|
||||
margin-top: 6px;
|
||||
color: var(--text-2);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.sem-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(103, 182, 255, 0.16);
|
||||
background: rgba(103, 182, 255, 0.08);
|
||||
color: #9ed9ff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sem-chip--warm {
|
||||
color: #ffce97;
|
||||
border-color: rgba(255, 179, 109, 0.16);
|
||||
background: rgba(255, 179, 109, 0.08);
|
||||
}
|
||||
|
||||
.sem-chip--success {
|
||||
color: #8bf0bf;
|
||||
border-color: rgba(80, 210, 159, 0.16);
|
||||
background: rgba(80, 210, 159, 0.08);
|
||||
}
|
||||
|
||||
.sem-command-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 14px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(132, 197, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.sem-command-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sem-segmented {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(132, 197, 255, 0.12);
|
||||
background: rgba(8, 16, 28, 0.58);
|
||||
}
|
||||
|
||||
.sem-button,
|
||||
.sem-button-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, opacity 180ms ease;
|
||||
}
|
||||
|
||||
.sem-button:hover,
|
||||
.sem-button-secondary:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sem-button {
|
||||
color: white;
|
||||
border: 1px solid rgba(103, 182, 255, 0.2);
|
||||
background: linear-gradient(180deg, rgba(53, 130, 245, 0.3), rgba(25, 88, 185, 0.18));
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sem-button-secondary {
|
||||
color: #d8e8fb;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sem-button:disabled,
|
||||
.sem-button-secondary:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sem-segmented button {
|
||||
min-height: 36px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sem-segmented button[data-active="true"] {
|
||||
background: linear-gradient(180deg, rgba(53, 130, 245, 0.34), rgba(25, 88, 185, 0.2));
|
||||
border-color: rgba(132, 197, 255, 0.2);
|
||||
}
|
||||
|
||||
.sem-input,
|
||||
.sem-select,
|
||||
.sem-textarea {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(132, 197, 255, 0.14);
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
color: var(--text-1);
|
||||
padding: 12px 14px;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.sem-input::placeholder,
|
||||
.sem-textarea::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
.sem-textarea {
|
||||
min-height: 160px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.sem-inspector {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sem-empty-state,
|
||||
.sem-loading-state {
|
||||
height: 100%;
|
||||
min-height: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 28px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.sem-empty-state-title,
|
||||
.sem-loading-state-title {
|
||||
color: var(--text-1);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 14px 0 6px;
|
||||
}
|
||||
|
||||
.sem-empty-state-copy,
|
||||
.sem-loading-state-copy {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-width: 42ch;
|
||||
}
|
||||
|
||||
.sem-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sem-list-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(132, 197, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
color: var(--text-1);
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sem-list-item:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(132, 197, 255, 0.18);
|
||||
background: rgba(103, 182, 255, 0.08);
|
||||
}
|
||||
|
||||
.sem-list-item[data-active="true"] {
|
||||
border-color: rgba(132, 197, 255, 0.22);
|
||||
background: linear-gradient(180deg, rgba(53, 130, 245, 0.18), rgba(25, 88, 185, 0.1));
|
||||
}
|
||||
|
||||
.sem-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
color: var(--text-1);
|
||||
}
|
||||
|
||||
.sem-table th {
|
||||
text-align: left;
|
||||
color: var(--text-2);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.sem-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.sem-grid-two,
|
||||
.sem-grid-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sem-workspace-hero {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sem-workspace-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
|
||||
type SurfaceTone = "default" | "subtle" | "accent";
|
||||
type SurfacePadding = "default" | "tight" | "none";
|
||||
type ChipTone = "default" | "warm" | "success";
|
||||
|
||||
function cx(...parts: Array<string | false | null | undefined>) {
|
||||
return parts.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export function WorkspaceFrame({
|
||||
kicker,
|
||||
title,
|
||||
subtitle,
|
||||
actions,
|
||||
children,
|
||||
}: {
|
||||
kicker?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="sem-workspace-frame">
|
||||
<header className="sem-workspace-hero">
|
||||
<div>
|
||||
{kicker ? <div className="sem-workspace-kicker">{kicker}</div> : null}
|
||||
<h2 className="sem-workspace-title">{title}</h2>
|
||||
{subtitle ? <p className="sem-workspace-subtitle">{subtitle}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="sem-workspace-actions">{actions}</div> : null}
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SurfaceCard({
|
||||
children,
|
||||
tone = "default",
|
||||
padding = "default",
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
tone?: SurfaceTone;
|
||||
padding?: SurfacePadding;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<div className={cx("sem-surface", tone !== "default" && `sem-surface--${tone}`, className)} style={style}>
|
||||
{padding === "none" ? children : <div className={cx("sem-surface-body", padding === "tight" && "sem-surface-body--tight")}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SectionHeader({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
}: {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="sem-section-header">
|
||||
<div>
|
||||
{eyebrow ? <div className="sem-section-eyebrow">{eyebrow}</div> : null}
|
||||
<h3 className="sem-section-title">{title}</h3>
|
||||
{description ? <p className="sem-section-copy">{description}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="sem-command-group">{actions}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetricChip({
|
||||
children,
|
||||
tone = "default",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
tone?: ChipTone;
|
||||
}) {
|
||||
return <span className={cx("sem-chip", tone !== "default" && `sem-chip--${tone}`)}>{children}</span>;
|
||||
}
|
||||
|
||||
export function CommandBar({
|
||||
left,
|
||||
right,
|
||||
}: {
|
||||
left?: ReactNode;
|
||||
right?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="sem-command-bar">
|
||||
<div className="sem-command-group">{left}</div>
|
||||
<div className="sem-command-group">{right}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InspectorPanel({
|
||||
children,
|
||||
open = true,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
open?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<SurfaceCard className={cx("sem-inspector", className)} padding="none" style={{ display: open ? "block" : "none" }}>
|
||||
{children}
|
||||
</SurfaceCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="sem-empty-state">
|
||||
{icon}
|
||||
<div className="sem-empty-state-title">{title}</div>
|
||||
<div className="sem-empty-state-copy">{description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingState({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="sem-loading-state">
|
||||
<div className="animate-spin" style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: "999px",
|
||||
border: "2px solid rgba(103, 182, 255, 0.18)",
|
||||
borderTopColor: "rgba(158, 217, 255, 0.92)",
|
||||
marginBottom: 12,
|
||||
}} />
|
||||
<div className="sem-loading-state-title">{title}</div>
|
||||
<div className="sem-loading-state-copy">{description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SegmentedControl({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return <div className="sem-segmented">{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx
|
||||
*/
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Scale, Search } from "lucide-react";
|
||||
|
||||
const THEME_CSS = `
|
||||
.glass-panel {
|
||||
background: linear-gradient(135deg, rgba(13,17,23,0.75), rgba(22,27,34,0.6));
|
||||
backdrop-filter: blur(16px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.2);
|
||||
border: 1px solid rgba(88,166,255,0.2);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 1px 1px 0 rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { opacity: 0.45; }
|
||||
50% { opacity: 0.85; }
|
||||
100% { opacity: 0.45; }
|
||||
}
|
||||
.skeleton-item {
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
animation: skeleton-shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
`;
|
||||
|
||||
type OutcomeKind = "approved" | "rejected" | "deferred" | "pending" | string;
|
||||
|
||||
function outcomeStyle(outcome: string): { color: string; bg: string; border: string } {
|
||||
const lower = (outcome ?? "").toLowerCase();
|
||||
if (lower.includes("approv") || lower.includes("accept"))
|
||||
return { color: "#4cc38a", bg: "rgba(76,195,138,0.12)", border: "rgba(76,195,138,0.28)" };
|
||||
if (lower.includes("reject") || lower.includes("denied") || lower.includes("fail"))
|
||||
return { color: "#ff7b72", bg: "rgba(255,123,114,0.12)", border: "rgba(255,123,114,0.28)" };
|
||||
if (lower.includes("defer") || lower.includes("pending") || lower.includes("review"))
|
||||
return { color: "#f2b66d", bg: "rgba(242,182,109,0.12)", border: "rgba(242,182,109,0.28)" };
|
||||
return { color: "#8fa8c6", bg: "rgba(143,168,198,0.08)", border: "rgba(143,168,198,0.18)" };
|
||||
}
|
||||
|
||||
function OutcomeBadge({ outcome }: { outcome: OutcomeKind }) {
|
||||
const style = outcomeStyle(outcome);
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "2px 8px",
|
||||
borderRadius: 999,
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
letterSpacing: "0.06em",
|
||||
textTransform: "uppercase",
|
||||
color: style.color,
|
||||
background: style.bg,
|
||||
border: `1px solid ${style.border}`,
|
||||
}}
|
||||
>
|
||||
{outcome || "unknown"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonList() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="skeleton-item" style={{ height: 62 }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Causal Flow Diagram ──────────────────────────────────────────── */
|
||||
|
||||
interface ChainStep {
|
||||
id: string;
|
||||
relationship: string;
|
||||
content?: string;
|
||||
type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function RelationshipPill({ label }: { label: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 0, position: "relative", margin: "0 auto" }}>
|
||||
{/* Connector line top */}
|
||||
<div style={{ width: 2, height: 12, background: "rgba(88,166,255,0.25)" }} />
|
||||
{/* Pill */}
|
||||
<div
|
||||
style={{
|
||||
padding: "3px 10px",
|
||||
borderRadius: 999,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.06em",
|
||||
textTransform: "uppercase",
|
||||
color: "#79c0ff",
|
||||
background: "rgba(88,166,255,0.1)",
|
||||
border: "1px solid rgba(88,166,255,0.22)",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 260,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{/* Connector line bottom + arrow */}
|
||||
<div style={{ width: 2, height: 10, background: "rgba(88,166,255,0.25)" }} />
|
||||
<div style={{ width: 0, height: 0, borderLeft: "5px solid transparent", borderRight: "5px solid transparent", borderTop: "6px solid rgba(88,166,255,0.4)" }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChainNodeCard({ step, index }: { step: ChainStep; index: number }) {
|
||||
const COLORS = ["#3E79F2", "#149287", "#2F9F61", "#555FD6", "#8A56D8", "#B65473", "#C9922E", "#4aa3ff"];
|
||||
const color = COLORS[index % COLORS.length];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
padding: "14px 16px",
|
||||
borderRadius: 12,
|
||||
background: "linear-gradient(135deg, rgba(13,17,23,0.75), rgba(22,27,34,0.5))",
|
||||
border: `1px solid ${color}33`,
|
||||
boxShadow: `0 0 0 1px ${color}11, inset 0 1px 0 rgba(255,255,255,0.04)`,
|
||||
borderLeft: `3px solid ${color}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 8, height: 8, borderRadius: "50%",
|
||||
background: color,
|
||||
boxShadow: `0 0 8px ${color}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
{step.type ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10, fontWeight: 700, letterSpacing: "0.06em",
|
||||
textTransform: "uppercase", color,
|
||||
}}
|
||||
>
|
||||
{step.type}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ color: "#e6edf3", fontSize: 14, fontWeight: 600 }}>
|
||||
{step.content || step.id}
|
||||
</div>
|
||||
{step.id && step.id !== step.content ? (
|
||||
<div style={{ color: "#6a7f97", fontSize: 11, fontFamily: "monospace", marginTop: 3 }}>{step.id}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CausalFlowDiagram({ chain, loading }: { chain: ChainStep[]; loading: boolean }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="skeleton-item" style={{ height: 68 }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (chain.length === 0) {
|
||||
return (
|
||||
<div style={{ textAlign: "center", padding: "40px 24px", color: "#8b949e", fontSize: 13 }}>
|
||||
No causal chain steps found for this decision.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "stretch" }}>
|
||||
{chain.map((step, index) => (
|
||||
<div key={`${step.id}-${index}`} style={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChainNodeCard step={step} index={index} />
|
||||
{index < chain.length - 1 ? (
|
||||
<RelationshipPill label={chain[index + 1]?.relationship || "→"} />
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Main Workspace ──────────────────────────────────────────────── */
|
||||
|
||||
export function DecisionWorkspace() {
|
||||
const [decisions, setDecisions] = useState<any[]>([]);
|
||||
const [selectedDecision, setSelectedDecision] = useState<any | null>(null);
|
||||
const [chain, setChain] = useState<ChainStep[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [listLoading, setListLoading] = useState(true);
|
||||
const [filterQuery, setFilterQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setListLoading(true);
|
||||
fetch("/api/decisions", { signal: controller.signal })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`Failed to load decisions: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setDecisions(data);
|
||||
if (data.length > 0) void handleSelectDecision(data[0]);
|
||||
})
|
||||
.catch((err) => { if (err.name !== "AbortError") console.error(err); })
|
||||
.finally(() => setListLoading(false));
|
||||
return () => controller.abort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const filteredDecisions = useMemo(() => {
|
||||
if (!filterQuery.trim()) return decisions;
|
||||
const q = filterQuery.toLowerCase();
|
||||
return decisions.filter(
|
||||
(d) =>
|
||||
String(d.decision_id ?? "").toLowerCase().includes(q) ||
|
||||
String(d.category ?? "").toLowerCase().includes(q) ||
|
||||
String(d.outcome ?? "").toLowerCase().includes(q),
|
||||
);
|
||||
}, [decisions, filterQuery]);
|
||||
|
||||
const handleSelectDecision = async (d: any) => {
|
||||
setSelectedDecision(d);
|
||||
setLoading(true);
|
||||
setChain([]);
|
||||
const controller = new AbortController();
|
||||
try {
|
||||
const res = await fetch(`/api/decisions/${encodeURIComponent(d.decision_id)}/chain`, { signal: controller.signal });
|
||||
if (!res.ok) throw new Error(`Failed to load chain: ${res.status}`);
|
||||
const data = await res.json();
|
||||
setChain(data.chain || []);
|
||||
} catch (e) {
|
||||
if ((e as DOMException).name !== "AbortError") console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return () => controller.abort();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", width: "100%", height: "100%", background: "#0d1117", overflow: "hidden" }}>
|
||||
<style>{THEME_CSS}</style>
|
||||
|
||||
{/* Left Column — Decision List */}
|
||||
<div
|
||||
className="glass-panel"
|
||||
style={{
|
||||
width: 300,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderRadius: 0,
|
||||
border: "none",
|
||||
borderRight: "1px solid rgba(88,166,255,0.16)",
|
||||
}}
|
||||
>
|
||||
{/* List header */}
|
||||
<div style={{ padding: "20px 20px 14px", borderBottom: "1px solid rgba(255,255,255,0.06)", flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
||||
<Scale size={16} color="#4aa3ff" />
|
||||
<h2 style={{ color: "#ebf3ff", margin: 0, fontSize: 15, fontWeight: 700 }}>Decisions</h2>
|
||||
{decisions.length > 0 ? (
|
||||
<span style={{ color: "#6a7f97", fontSize: 11, marginLeft: "auto" }}>{decisions.length}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Filter input */}
|
||||
<div style={{ position: "relative" }}>
|
||||
<Search
|
||||
size={13}
|
||||
color="#8b949e"
|
||||
style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", pointerEvents: "none" }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter decisions…"
|
||||
value={filterQuery}
|
||||
onChange={(e) => setFilterQuery(e.target.value)}
|
||||
style={filterInputStyle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decision list */}
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "12px 14px" }}>
|
||||
{listLoading ? (
|
||||
<SkeletonList />
|
||||
) : filteredDecisions.length === 0 ? (
|
||||
<div style={{ color: "#6a7f97", fontSize: 13, textAlign: "center", padding: "32px 12px" }}>
|
||||
{decisions.length === 0 ? "No decisions available." : "No decisions match your filter."}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{filteredDecisions.map((d) => {
|
||||
const isActive = selectedDecision?.decision_id === d.decision_id;
|
||||
return (
|
||||
<button
|
||||
key={d.decision_id}
|
||||
onClick={() => void handleSelectDecision(d)}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "10px 12px",
|
||||
borderRadius: 10,
|
||||
cursor: "pointer",
|
||||
background: isActive
|
||||
? "rgba(74,163,255,0.15)"
|
||||
: "rgba(255,255,255,0.02)",
|
||||
border: isActive
|
||||
? "1px solid rgba(74,163,255,0.32)"
|
||||
: "1px solid rgba(255,255,255,0.06)",
|
||||
color: isActive ? "#ffffff" : "#c6d4e3",
|
||||
transition: "all 160ms ease",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 700, fontSize: 13, marginBottom: 4 }}>{d.decision_id}</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
|
||||
{d.category ? (
|
||||
<span style={{ fontSize: 11, color: "#8b949e" }}>{d.category}</span>
|
||||
) : null}
|
||||
{d.outcome ? <OutcomeBadge outcome={d.outcome} /> : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column — Decision Detail */}
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
{/* Radial accent */}
|
||||
<div style={{ position: "absolute", inset: 0, background: "radial-gradient(ellipse at top right, rgba(88,166,255,0.04), transparent 55%)", pointerEvents: "none", zIndex: 0 }} />
|
||||
|
||||
{selectedDecision ? (
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "28px 32px", position: "relative", zIndex: 1 }}>
|
||||
{/* Decision header */}
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 14, flexWrap: "wrap" }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ color: "#8b949e", fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 6 }}>
|
||||
Decision ID
|
||||
</div>
|
||||
<h1 style={{ color: "#ffffff", fontSize: 24, fontWeight: 800, letterSpacing: "-0.03em", margin: "0 0 8px 0", wordBreak: "break-word" }}>
|
||||
{selectedDecision.decision_id}
|
||||
</h1>
|
||||
</div>
|
||||
{selectedDecision.outcome ? <OutcomeBadge outcome={selectedDecision.outcome} /> : null}
|
||||
</div>
|
||||
|
||||
{selectedDecision.category ? (
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "4px 10px", borderRadius: 999, background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)", color: "#8b949e", fontSize: 12 }}>
|
||||
{selectedDecision.category}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Causal Chain */}
|
||||
<div className="glass-panel" style={{ padding: 24, borderRadius: 16 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 20 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: "50%", background: "linear-gradient(135deg, #4aa3ff, #f2b66d)", boxShadow: "0 0 10px rgba(74,163,255,0.4)" }} />
|
||||
<h3 style={{ color: "#e6edf3", margin: 0, fontSize: 14, fontWeight: 700, letterSpacing: "0.02em" }}>
|
||||
Causal Chain
|
||||
</h3>
|
||||
{chain.length > 0 && !loading ? (
|
||||
<span style={{ color: "#6a7f97", fontSize: 11, marginLeft: "auto" }}>
|
||||
{chain.length} step{chain.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<CausalFlowDiagram chain={chain} loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#8b949e", fontSize: 14 }}>
|
||||
Select a decision to inspect its causal chain.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── styles ─────────────────────────────────────────────────────── */
|
||||
|
||||
const filterInputStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
padding: "7px 10px 7px 30px",
|
||||
background: "rgba(0,0,0,0.25)",
|
||||
border: "1px solid rgba(88,166,255,0.16)",
|
||||
borderRadius: 8,
|
||||
color: "#c6d4e3",
|
||||
fontSize: 12,
|
||||
outline: "none",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { logEvent } from "../../store/registryStore";
|
||||
|
||||
const THEME_CSS = `
|
||||
.glass-panel {
|
||||
background: linear-gradient(135deg, rgba(13,17,23,0.75), rgba(22,27,34,0.6));
|
||||
backdrop-filter: blur(16px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.2);
|
||||
border: 1px solid rgba(88,166,255,0.2);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 1px 1px 0 rgba(255,255,255,0.05);
|
||||
}
|
||||
`;
|
||||
|
||||
export function DiffMergeWorkspace() {
|
||||
const [primaryId, setPrimaryId] = useState("n-primary-1");
|
||||
const [duplicateId, setDuplicateId] = useState("n-dup-2");
|
||||
|
||||
const [msg, setMsg] = useState("");
|
||||
|
||||
const handleMerge = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/enrich/merge", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ primary_id: primaryId, duplicate_ids: [duplicateId] })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.merged_into) {
|
||||
setMsg(`Merge success: redirected ${data.edges_updated} edges to ${data.merged_into}`);
|
||||
logEvent("merge", `Merged ${duplicateId} → ${data.merged_into} · ${data.edges_updated} edges redirected`, {
|
||||
primary: data.merged_into,
|
||||
duplicate: duplicateId,
|
||||
edgesUpdated: data.edges_updated,
|
||||
});
|
||||
} else {
|
||||
setMsg("Merge failed...");
|
||||
}
|
||||
} catch (err) {
|
||||
setMsg("Error calling merge endpoint.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", width: "100%", height: "100%", background: "#0d1117", padding: 32, gap: 24, boxSizing: "border-box" }}>
|
||||
<style>{THEME_CSS}</style>
|
||||
<div>
|
||||
<h1 style={{ margin: "0 0 8px 0", color: "#fff" }}>Entity Diff & Merge</h1>
|
||||
<p style={{ margin: 0, color: "#8b949e" }}>Compare suspected duplicate entities and reconcile them.</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 24, flex: 1 }}>
|
||||
{/* Primary View */}
|
||||
<div className="glass-panel" style={{ flex: 1, borderRadius: 12, padding: 24 }}>
|
||||
<h3 style={{ color: "#58a6ff", margin: "0 0 16px 0", borderBottom: "1px solid rgba(88,166,255,0.2)", paddingBottom: 8 }}>Primary Entity</h3>
|
||||
<label style={{ display: "block", color: "#c9d1d9", marginBottom: 8, fontSize: 13 }}>Primary Node ID</label>
|
||||
<input
|
||||
value={primaryId} onChange={e => setPrimaryId(e.target.value)}
|
||||
style={{ width: "100%", background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)", color: "#fff", padding: "8px 12px", borderRadius: 6, marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
<div style={{ background: "rgba(0,0,0,0.2)", padding: 16, borderRadius: 6 }}>
|
||||
<div style={{ color: "#8b949e", fontSize: 12, marginBottom: 4 }}>Name</div>
|
||||
<div style={{ color: "#fff", fontSize: 14 }}>Sample Company Inc.</div>
|
||||
|
||||
<div style={{ color: "#8b949e", fontSize: 12, marginTop: 16, marginBottom: 4 }}>Founded</div>
|
||||
<div style={{ color: "#fff", fontSize: 14 }}>2004-05-12</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Duplicate View */}
|
||||
<div className="glass-panel" style={{ flex: 1, borderRadius: 12, padding: 24 }}>
|
||||
<h3 style={{ color: "#ff7b72", margin: "0 0 16px 0", borderBottom: "1px solid rgba(255,123,114,0.2)", paddingBottom: 8 }}>Duplicate Entity</h3>
|
||||
<label style={{ display: "block", color: "#c9d1d9", marginBottom: 8, fontSize: 13 }}>Duplicate Node ID</label>
|
||||
<input
|
||||
value={duplicateId} onChange={e => setDuplicateId(e.target.value)}
|
||||
style={{ width: "100%", background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)", color: "#fff", padding: "8px 12px", borderRadius: 6, marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
<div style={{ background: "rgba(0,0,0,0.2)", padding: 16, borderRadius: 6 }}>
|
||||
<div style={{ color: "#d2a8ff", fontSize: 12, marginBottom: 4 }}>Name</div>
|
||||
{/* Amber highlight for differing values */}
|
||||
<div style={{ color: "#d29922", fontSize: 14, fontWeight: "bold" }}>Sample Company</div>
|
||||
|
||||
<div style={{ color: "#8b949e", fontSize: 12, marginTop: 16, marginBottom: 4 }}>Founded</div>
|
||||
<div style={{ color: "#fff", fontSize: 14 }}>2004-05-12</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div style={{ color: "#58a6ff" }}>{msg}</div>
|
||||
<button
|
||||
onClick={handleMerge}
|
||||
style={{ background: "#238636", color: "#fff", border: "none", padding: "10px 24px", borderRadius: 6, fontWeight: 600, cursor: "pointer", fontSize: 16 }}
|
||||
>
|
||||
Confirm Merge
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx
|
||||
*
|
||||
* Entity Resolution — run duplicate detection, review flagged pairs,
|
||||
* perform one-click merges, and view merge history from the Registry.
|
||||
*/
|
||||
import { useState, useCallback } from "react";
|
||||
import { ScanSearch, GitMerge, X, ChevronDown, ChevronRight, Loader2 } from "lucide-react";
|
||||
import { logEvent, useRegistry } from "../../store/registryStore";
|
||||
|
||||
interface DedupPair {
|
||||
a: { id: string; label: string; type: string };
|
||||
b: { id: string; label: string; type: string };
|
||||
score: number;
|
||||
dismissed?: boolean;
|
||||
}
|
||||
|
||||
interface RawDuplicateItem {
|
||||
entity_a?: string | Record<string, unknown>;
|
||||
entity_b?: string | Record<string, unknown>;
|
||||
similarity?: number;
|
||||
score?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function extractId(entity: string | Record<string, unknown> | undefined): string {
|
||||
if (!entity) return "";
|
||||
if (typeof entity === "string") return entity;
|
||||
return String(entity.id ?? entity.text ?? JSON.stringify(entity));
|
||||
}
|
||||
|
||||
function extractLabel(entity: string | Record<string, unknown> | undefined): string {
|
||||
if (!entity) return "";
|
||||
if (typeof entity === "string") return entity;
|
||||
return String(entity.text ?? entity.label ?? entity.content ?? entity.id ?? "");
|
||||
}
|
||||
|
||||
function extractType(entity: string | Record<string, unknown> | undefined): string {
|
||||
if (!entity || typeof entity === "string") return "entity";
|
||||
return String(entity.type ?? "entity");
|
||||
}
|
||||
|
||||
function parseDuplicates(raw: RawDuplicateItem[]): DedupPair[] {
|
||||
return raw.map((item) => ({
|
||||
a: {
|
||||
id: extractId(item.entity_a as string | Record<string, unknown>),
|
||||
label: extractLabel(item.entity_a as string | Record<string, unknown>),
|
||||
type: extractType(item.entity_a as string | Record<string, unknown>),
|
||||
},
|
||||
b: {
|
||||
id: extractId(item.entity_b as string | Record<string, unknown>),
|
||||
label: extractLabel(item.entity_b as string | Record<string, unknown>),
|
||||
type: extractType(item.entity_b as string | Record<string, unknown>),
|
||||
},
|
||||
score: Number(item.similarity ?? item.score ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
function ScoreBar({ score }: { score: number }) {
|
||||
const pct = Math.min(100, Math.round(score * 100));
|
||||
const color = score >= 0.9 ? "#ff7b72" : score >= 0.75 ? "#f2b66d" : "#4cc38a";
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<div style={{ flex: 1, height: 4, borderRadius: 999, background: "rgba(255,255,255,0.06)", overflow: "hidden" }}>
|
||||
<div style={{ width: `${pct}%`, height: "100%", borderRadius: 999, background: color, transition: "width 300ms ease" }} />
|
||||
</div>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color, minWidth: 34, textAlign: "right" }}>
|
||||
{pct}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PairRow({
|
||||
pair,
|
||||
onMerge,
|
||||
onDismiss,
|
||||
}: {
|
||||
pair: DedupPair;
|
||||
onMerge: (primaryId: string, duplicateId: string) => Promise<void>;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [merging, setMerging] = useState(false);
|
||||
|
||||
const handleMerge = async () => {
|
||||
setMerging(true);
|
||||
await onMerge(pair.a.id, pair.b.id);
|
||||
setMerging(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={pairCardStyle}>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>
|
||||
{/* Expand */}
|
||||
<button onClick={() => setExpanded((v) => !v)} style={iconBtnStyle}>
|
||||
{expanded ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
|
||||
</button>
|
||||
|
||||
{/* Entity Labels */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
<span style={entityChipStyle}>{pair.a.label || pair.a.id}</span>
|
||||
<span style={{ color: "#f2b66d", fontSize: 12, fontWeight: 700 }}>≈</span>
|
||||
<span style={entityChipStyle}>{pair.b.label || pair.b.id}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<ScoreBar score={pair.score} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={() => void handleMerge()}
|
||||
disabled={merging}
|
||||
style={{
|
||||
...actionBtnStyle,
|
||||
background: "rgba(76,195,138,0.12)",
|
||||
border: "1px solid rgba(76,195,138,0.28)",
|
||||
color: "#4cc38a",
|
||||
}}
|
||||
>
|
||||
{merging ? <Loader2 size={12} className="animate-spin" /> : <GitMerge size={12} />}
|
||||
<span>Merge</span>
|
||||
</button>
|
||||
<button onClick={onDismiss} style={iconBtnStyle} title="Dismiss">
|
||||
<X size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded diff */}
|
||||
{expanded ? (
|
||||
<div style={{ marginTop: 12, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
|
||||
{[
|
||||
{ label: "Primary (keep)", entity: pair.a, accentColor: "#4aa3ff" },
|
||||
{ label: "Duplicate (remove)", entity: pair.b, accentColor: "#ff7b72" },
|
||||
].map(({ label, entity, accentColor }) => (
|
||||
<div key={entity.id} style={{ ...diffCardStyle, borderColor: `${accentColor}33` }}>
|
||||
<div style={{ color: accentColor, fontSize: 10, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase", marginBottom: 6 }}>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{ color: "#e6edf3", fontSize: 13, fontWeight: 600 }}>{entity.label || entity.id}</div>
|
||||
<div style={{ color: "#8b949e", fontSize: 11, marginTop: 3 }}>{entity.type}</div>
|
||||
<div style={{ color: "#6a7f97", fontSize: 10, marginTop: 4, fontFamily: "monospace" }}>{entity.id}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EntityResolutionTab() {
|
||||
const [threshold, setThreshold] = useState(0.82);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [pairs, setPairs] = useState<DedupPair[]>([]);
|
||||
const [scanError, setScanError] = useState("");
|
||||
const registryEntries = useRegistry();
|
||||
|
||||
const mergeHistory = registryEntries.filter((e) => e.op === "merge");
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
setScanning(true);
|
||||
setScanError("");
|
||||
try {
|
||||
const res = await fetch("/api/enrich/dedup", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ threshold }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error((err as Record<string, string>).detail ?? `Scan failed (${res.status})`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const rawDuplicates: RawDuplicateItem[] = Array.isArray(data.duplicates)
|
||||
? (data.duplicates as RawDuplicateItem[])
|
||||
: [];
|
||||
const parsed = parseDuplicates(rawDuplicates);
|
||||
setPairs(parsed);
|
||||
logEvent("import", `Dedup scan found ${parsed.length} flagged pair${parsed.length !== 1 ? "s" : ""} (threshold ${threshold.toFixed(2)})`, {
|
||||
threshold,
|
||||
flagged: parsed.length,
|
||||
});
|
||||
} catch (err) {
|
||||
setScanError(err instanceof Error ? err.message : "Scan failed");
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
}, [threshold]);
|
||||
|
||||
const handleMerge = useCallback(async (primaryId: string, duplicateId: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/enrich/merge", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ primary_id: primaryId, duplicate_ids: [duplicateId] }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Merge failed (${res.status})`);
|
||||
const data = await res.json();
|
||||
logEvent("merge", `Merged ${duplicateId} → ${primaryId} · ${data.edges_updated ?? 0} edges redirected`, {
|
||||
primary: primaryId,
|
||||
duplicate: duplicateId,
|
||||
edgesUpdated: data.edges_updated,
|
||||
});
|
||||
setPairs((prev) => prev.filter((p) => !(p.a.id === primaryId && p.b.id === duplicateId)));
|
||||
} catch (err) {
|
||||
console.error("[EntityResolution] merge failed", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDismiss = useCallback((index: number) => {
|
||||
setPairs((prev) => prev.filter((_, i) => i !== index));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={shellStyle}>
|
||||
{/* Header */}
|
||||
<div style={headerStyle}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<ScanSearch size={18} color="#f2b66d" />
|
||||
<div>
|
||||
<div style={{ color: "#ebf3ff", fontSize: 16, fontWeight: 700 }}>Entity Resolution</div>
|
||||
<div style={{ color: "#8b949e", fontSize: 12 }}>Detect and merge duplicate entities in the knowledge graph</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scan controls */}
|
||||
<div style={controlsCardStyle}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
|
||||
<div style={{ flex: 1, minWidth: 240 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
|
||||
<label style={{ color: "#c6d4e3", fontSize: 12, fontWeight: 600 }}>Similarity Threshold</label>
|
||||
<span style={{ color: "#f2b66d", fontSize: 12, fontWeight: 700 }}>{threshold.toFixed(2)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0.5}
|
||||
max={0.99}
|
||||
step={0.01}
|
||||
value={threshold}
|
||||
onChange={(e) => setThreshold(parseFloat(e.target.value))}
|
||||
style={{ width: "100%", accentColor: "#f2b66d", cursor: "pointer" }}
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", color: "#6a7f97", fontSize: 10, marginTop: 2 }}>
|
||||
<span>More results (0.50)</span>
|
||||
<span>Fewer, higher confidence (0.99)</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => void handleScan()}
|
||||
disabled={scanning}
|
||||
style={scanBtnStyle}
|
||||
>
|
||||
{scanning ? <Loader2 size={14} className="animate-spin" /> : <ScanSearch size={14} />}
|
||||
<span>{scanning ? "Scanning…" : "Run Dedup Scan"}</span>
|
||||
</button>
|
||||
</div>
|
||||
{scanError ? (
|
||||
<div style={{ color: "#ff7b72", fontSize: 12, marginTop: 8 }}>{scanError}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: "hidden", display: "flex", gap: 0 }}>
|
||||
{/* Flagged pairs */}
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "16px 24px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{pairs.length > 0 ? (
|
||||
<>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }}>
|
||||
<div style={{ color: "#8b949e", fontSize: 12, fontWeight: 600 }}>
|
||||
{pairs.length} flagged pair{pairs.length !== 1 ? "s" : ""}
|
||||
</div>
|
||||
<button onClick={() => setPairs([])} style={clearAllBtnStyle}>Clear all</button>
|
||||
</div>
|
||||
{pairs.map((pair, index) => (
|
||||
<PairRow
|
||||
key={`${pair.a.id}:${pair.b.id}`}
|
||||
pair={pair}
|
||||
onMerge={handleMerge}
|
||||
onDismiss={() => handleDismiss(index)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div style={emptyStateStyle}>
|
||||
<ScanSearch size={36} color="rgba(242,182,109,0.15)" />
|
||||
<div style={{ color: "#8b949e", fontSize: 14, marginTop: 12, fontWeight: 500 }}>
|
||||
No flagged pairs
|
||||
</div>
|
||||
<div style={{ color: "#6a7f97", fontSize: 12, marginTop: 4, textAlign: "center", maxWidth: 280 }}>
|
||||
Set a similarity threshold and run a dedup scan to detect potential duplicates.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Merge history sidebar */}
|
||||
{mergeHistory.length > 0 ? (
|
||||
<div style={historyPanelStyle}>
|
||||
<div style={{ color: "#8b949e", fontSize: 11, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase", marginBottom: 10 }}>
|
||||
Merge History
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{mergeHistory.map((entry) => (
|
||||
<div key={entry.id} style={historyRowStyle}>
|
||||
<GitMerge size={11} color="#f2b66d" />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ color: "#c6d4e3", fontSize: 11, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{entry.summary}
|
||||
</div>
|
||||
<div style={{ color: "#6a7f97", fontSize: 10 }}>
|
||||
{entry.timestamp.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── styles ─────────────────────────────────────────────────────── */
|
||||
|
||||
const shellStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "#0d1117",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
padding: "20px 24px 16px",
|
||||
borderBottom: "1px solid rgba(88,166,255,0.1)",
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const controlsCardStyle: React.CSSProperties = {
|
||||
margin: "16px 24px",
|
||||
padding: "16px 20px",
|
||||
borderRadius: 14,
|
||||
background: "linear-gradient(135deg, rgba(13,17,23,0.75), rgba(22,27,34,0.6))",
|
||||
border: "1px solid rgba(242,182,109,0.18)",
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const scanBtnStyle: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 7,
|
||||
padding: "10px 18px",
|
||||
borderRadius: 10,
|
||||
background: "linear-gradient(135deg, rgba(242,182,109,0.22), rgba(242,182,109,0.1))",
|
||||
border: "1px solid rgba(242,182,109,0.32)",
|
||||
color: "#f2b66d",
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
cursor: "pointer",
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const pairCardStyle: React.CSSProperties = {
|
||||
padding: "12px 14px",
|
||||
borderRadius: 12,
|
||||
background: "linear-gradient(135deg, rgba(13,17,23,0.6), rgba(22,27,34,0.4))",
|
||||
border: "1px solid rgba(255,255,255,0.07)",
|
||||
};
|
||||
|
||||
const entityChipStyle: React.CSSProperties = {
|
||||
display: "inline-block",
|
||||
padding: "4px 10px",
|
||||
borderRadius: 8,
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
color: "#e6edf3",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const actionBtnStyle: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
padding: "5px 10px",
|
||||
borderRadius: 8,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const iconBtnStyle: React.CSSProperties = {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#8b949e",
|
||||
cursor: "pointer",
|
||||
padding: 4,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
};
|
||||
|
||||
const diffCardStyle: React.CSSProperties = {
|
||||
padding: "10px 12px",
|
||||
borderRadius: 10,
|
||||
background: "rgba(0,0,0,0.2)",
|
||||
border: "1px solid transparent",
|
||||
};
|
||||
|
||||
const historyPanelStyle: React.CSSProperties = {
|
||||
width: 240,
|
||||
borderLeft: "1px solid rgba(255,255,255,0.06)",
|
||||
padding: "16px 16px",
|
||||
overflowY: "auto",
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const historyRowStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 7,
|
||||
padding: "8px 0",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
||||
};
|
||||
|
||||
const emptyStateStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 40,
|
||||
minHeight: 200,
|
||||
};
|
||||
|
||||
const clearAllBtnStyle: React.CSSProperties = {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#8b949e",
|
||||
fontSize: 12,
|
||||
cursor: "pointer",
|
||||
padding: "2px 6px",
|
||||
borderRadius: 6,
|
||||
};
|
||||
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* src/workspaces/EnrichWorkspace/RegistryTab.tsx
|
||||
*
|
||||
* Document Registry — a live, filterable chronological audit log of every
|
||||
* KG / Ontology mutation that occurred in this session.
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { ClipboardList, Filter, Trash2, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useRegistry, clearRegistry, type RegistryEntryOp } from "../../store/registryStore";
|
||||
|
||||
const OP_META: Record<
|
||||
RegistryEntryOp,
|
||||
{ label: string; color: string; bg: string; border: string }
|
||||
> = {
|
||||
import: { label: "IMPORT", color: "#4aa3ff", bg: "rgba(74,163,255,0.12)", border: "rgba(74,163,255,0.28)" },
|
||||
export: { label: "EXPORT", color: "#8fa8c6", bg: "rgba(143,168,198,0.08)", border: "rgba(143,168,198,0.18)" },
|
||||
merge: { label: "MERGE", color: "#f2b66d", bg: "rgba(242,182,109,0.12)", border: "rgba(242,182,109,0.28)" },
|
||||
"add-node": { label: "ADD NODE", color: "#4cc38a", bg: "rgba(76,195,138,0.12)", border: "rgba(76,195,138,0.28)" },
|
||||
"add-edge": { label: "ADD EDGE", color: "#4cc38a", bg: "rgba(76,195,138,0.10)", border: "rgba(76,195,138,0.22)" },
|
||||
delete: { label: "DELETE", color: "#ff7b72", bg: "rgba(255,123,114,0.12)", border: "rgba(255,123,114,0.28)" },
|
||||
infer: { label: "INFER", color: "#d2a8ff", bg: "rgba(210,168,255,0.12)", border: "rgba(210,168,255,0.28)" },
|
||||
"vocab-import": { label: "VOCAB", color: "#79c0ff", bg: "rgba(121,192,255,0.12)", border: "rgba(121,192,255,0.28)" },
|
||||
};
|
||||
|
||||
const ALL_OPS: (RegistryEntryOp | "all")[] = [
|
||||
"all", "import", "export", "merge", "add-node", "add-edge", "infer", "delete", "vocab-import",
|
||||
];
|
||||
|
||||
function formatTimestamp(date: Date): string {
|
||||
return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
function EntryRow({ entry }: { entry: ReturnType<typeof useRegistry>[number] }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const meta = OP_META[entry.op];
|
||||
const hasDetail = entry.detail && Object.keys(entry.detail).length > 0;
|
||||
|
||||
return (
|
||||
<div style={entryCardStyle}>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>
|
||||
{/* Op Badge */}
|
||||
<span
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
display: "inline-block",
|
||||
padding: "3px 8px",
|
||||
borderRadius: 999,
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
letterSpacing: "0.07em",
|
||||
color: meta.color,
|
||||
background: meta.bg,
|
||||
border: `1px solid ${meta.border}`,
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
{meta.label}
|
||||
</span>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ color: "#e6edf3", fontSize: 13, fontWeight: 500, wordBreak: "break-word" }}>
|
||||
{entry.summary}
|
||||
</div>
|
||||
<div style={{ color: "#8b949e", fontSize: 11, marginTop: 3 }}>
|
||||
{formatDate(entry.timestamp)} · {formatTimestamp(entry.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expand toggle */}
|
||||
{hasDetail ? (
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
title={expanded ? "Collapse details" : "Expand details"}
|
||||
style={expandBtnStyle}
|
||||
>
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Expanded detail */}
|
||||
{expanded && hasDetail ? (
|
||||
<pre style={detailPreStyle}>
|
||||
{JSON.stringify(entry.detail, null, 2)}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RegistryTab() {
|
||||
const entries = useRegistry();
|
||||
const [activeFilter, setActiveFilter] = useState<RegistryEntryOp | "all">("all");
|
||||
|
||||
const filtered = activeFilter === "all"
|
||||
? entries
|
||||
: entries.filter((e) => e.op === activeFilter);
|
||||
|
||||
return (
|
||||
<div style={shellStyle}>
|
||||
{/* Header */}
|
||||
<div style={headerStyle}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<ClipboardList size={18} color="#4aa3ff" />
|
||||
<div>
|
||||
<div style={{ color: "#ebf3ff", fontSize: 16, fontWeight: 700 }}>Document Registry</div>
|
||||
<div style={{ color: "#8b949e", fontSize: 12 }}>
|
||||
Audit log of all KG and Ontology mutations this session
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ color: "#8fa8c6", fontSize: 12 }}>
|
||||
{entries.length} event{entries.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
{entries.length > 0 ? (
|
||||
<button
|
||||
onClick={clearRegistry}
|
||||
title="Clear all events"
|
||||
style={clearBtnStyle}
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter pills */}
|
||||
<div style={filterBarStyle}>
|
||||
<Filter size={13} color="#8fa8c6" />
|
||||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
||||
{ALL_OPS.map((op) => {
|
||||
const isActive = op === activeFilter;
|
||||
const meta = op === "all" ? null : OP_META[op as RegistryEntryOp];
|
||||
return (
|
||||
<button
|
||||
key={op}
|
||||
onClick={() => setActiveFilter(op as typeof activeFilter)}
|
||||
style={{
|
||||
padding: "4px 10px",
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
border: isActive
|
||||
? `1px solid ${meta?.border ?? "rgba(127,208,255,0.35)"}`
|
||||
: "1px solid rgba(255,255,255,0.06)",
|
||||
background: isActive
|
||||
? (meta?.bg ?? "rgba(74,163,255,0.14)")
|
||||
: "transparent",
|
||||
color: isActive
|
||||
? (meta?.color ?? "#8ed3ff")
|
||||
: "#8b949e",
|
||||
transition: "all 140ms ease",
|
||||
}}
|
||||
>
|
||||
{op === "all" ? "All" : (meta?.label ?? op)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feed */}
|
||||
<div style={feedStyle}>
|
||||
{filtered.length === 0 ? (
|
||||
<div style={emptyStateStyle}>
|
||||
<ClipboardList size={36} color="rgba(127,208,255,0.15)" />
|
||||
<div style={{ color: "#8b949e", fontSize: 14, marginTop: 12, fontWeight: 500 }}>
|
||||
No events recorded yet
|
||||
</div>
|
||||
<div style={{ color: "#6a7f97", fontSize: 12, marginTop: 4, textAlign: "center", maxWidth: 300 }}>
|
||||
Import a file, run reasoning, or merge entities to see activity appear here.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((entry) => <EntryRow key={entry.id} entry={entry} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── styles ─────────────────────────────────────────────────────── */
|
||||
|
||||
const shellStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "#0d1117",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "20px 24px 16px",
|
||||
borderBottom: "1px solid rgba(88,166,255,0.1)",
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const filterBarStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
padding: "12px 24px",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.05)",
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const feedStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
padding: "16px 24px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
};
|
||||
|
||||
const entryCardStyle: React.CSSProperties = {
|
||||
padding: "12px 14px",
|
||||
borderRadius: 12,
|
||||
background: "linear-gradient(135deg, rgba(13,17,23,0.6), rgba(22,27,34,0.4))",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.03)",
|
||||
};
|
||||
|
||||
const expandBtnStyle: React.CSSProperties = {
|
||||
flexShrink: 0,
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#8b949e",
|
||||
cursor: "pointer",
|
||||
padding: 4,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
};
|
||||
|
||||
const detailPreStyle: React.CSSProperties = {
|
||||
marginTop: 10,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,0,0,0.28)",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
color: "#79c0ff",
|
||||
fontSize: 11,
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
overflowX: "auto",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
};
|
||||
|
||||
const clearBtnStyle: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
padding: "5px 10px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255,123,114,0.22)",
|
||||
background: "rgba(255,123,114,0.06)",
|
||||
color: "#ff7b72",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const emptyStateStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 40,
|
||||
minHeight: 280,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,746 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { graph } from "../../store/graphStore";
|
||||
import { GRAPH_THEME, withAlpha } from "./graphTheme";
|
||||
import type { GraphSelectedNodeKind } from "./types";
|
||||
|
||||
export type LinkPrediction = {
|
||||
target: string;
|
||||
type: string;
|
||||
label?: string;
|
||||
score: number;
|
||||
};
|
||||
|
||||
export type PathResponse = {
|
||||
path: string[];
|
||||
edge_ids?: string[];
|
||||
total_weight: number;
|
||||
hop_count: number;
|
||||
distance_band: "direct" | "near" | "mid-range" | "distant";
|
||||
// FR-1 distance intelligence enrichment
|
||||
semantic_similarity?: number | null;
|
||||
path_coherence_score?: number | null;
|
||||
confidence_decay?: number | null;
|
||||
bottleneck_node?: string | null;
|
||||
alternative_path_count?: number;
|
||||
interpretation?: string;
|
||||
};
|
||||
|
||||
export interface GraphInspectorPanelProps {
|
||||
nodeId: string;
|
||||
inspectableNodeId?: string | null;
|
||||
selectedNodeKind?: GraphSelectedNodeKind;
|
||||
canActivateFocused?: boolean;
|
||||
focusedUnavailableReason?: string | null;
|
||||
predictions: LinkPrediction[];
|
||||
predictionType: string;
|
||||
onPredictionTypeChange: (value: string) => void;
|
||||
onRunPredictions: () => void;
|
||||
isRunningPredictions?: boolean;
|
||||
pathTargetId: string;
|
||||
onPathTargetChange: (value: string) => void;
|
||||
onTracePath: () => void;
|
||||
pathResult: PathResponse | null;
|
||||
onDownloadProvenance: (format: "json" | "markdown") => void;
|
||||
onFocusNode?: (nodeId: string) => void;
|
||||
}
|
||||
|
||||
const PROVENANCE_KEYS = ["source", "source_url", "pmid", "pmids", "evidence", "provenance", "confidence"] as const;
|
||||
|
||||
function sourceAttribution(properties: Record<string, unknown>) {
|
||||
return PROVENANCE_KEYS
|
||||
.filter((key) => key in properties)
|
||||
.map((key) => ({ key, value: properties[key] }));
|
||||
}
|
||||
|
||||
/* ─── Path Distance Intelligence Panel ──────────────────────────── */
|
||||
|
||||
const BAND_COLORS: Record<string, string> = {
|
||||
direct: "#3fb950",
|
||||
near: "#79c0ff",
|
||||
"mid-range": "#e3b341",
|
||||
distant: "#ff7b72",
|
||||
};
|
||||
|
||||
function PathDistanceIntelPanel({ result }: { result: PathResponse }) {
|
||||
const bandColor = BAND_COLORS[result.distance_band] ?? "#8b949e";
|
||||
const hasMetrics =
|
||||
result.confidence_decay != null ||
|
||||
result.semantic_similarity != null ||
|
||||
result.path_coherence_score != null ||
|
||||
result.bottleneck_node != null;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
|
||||
{/* distance band + alt paths */}
|
||||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
|
||||
<span
|
||||
style={{
|
||||
padding: "3px 8px",
|
||||
borderRadius: 999,
|
||||
background: withAlpha(bandColor, 0.14),
|
||||
border: `1px solid ${withAlpha(bandColor, 0.3)}`,
|
||||
color: bandColor,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{result.distance_band} · {result.hop_count} hop{result.hop_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
{(result.alternative_path_count ?? 0) > 0 && (
|
||||
<span style={subtleChipStyle}>{result.alternative_path_count} alt path{result.alternative_path_count !== 1 ? "s" : ""}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* metric grid */}
|
||||
{hasMetrics && <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
|
||||
{result.confidence_decay != null && (
|
||||
<div style={metricCardStyle}>
|
||||
<div style={metricLabelStyle}>Confidence Decay</div>
|
||||
<div
|
||||
style={{
|
||||
...metricValueStyle,
|
||||
color: result.confidence_decay > 0.6 ? "#3fb950" : result.confidence_decay > 0.3 ? "#e3b341" : "#ff7b72",
|
||||
}}
|
||||
>
|
||||
{(result.confidence_decay * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div style={metricBarTrackStyle}>
|
||||
<div
|
||||
style={{
|
||||
...metricBarFillStyle,
|
||||
width: `${result.confidence_decay * 100}%`,
|
||||
background:
|
||||
result.confidence_decay > 0.6 ? "#3fb950" : result.confidence_decay > 0.3 ? "#e3b341" : "#ff7b72",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{result.semantic_similarity != null && (
|
||||
<div style={metricCardStyle}>
|
||||
<div style={metricLabelStyle}>Semantic Sim.</div>
|
||||
<div style={{ ...metricValueStyle, color: "#79c0ff" }}>
|
||||
{(result.semantic_similarity * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div style={metricBarTrackStyle}>
|
||||
<div style={{ ...metricBarFillStyle, width: `${result.semantic_similarity * 100}%`, background: "#79c0ff" }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{result.path_coherence_score != null && (
|
||||
<div style={metricCardStyle}>
|
||||
<div style={metricLabelStyle}>Path Coherence</div>
|
||||
<div style={{ ...metricValueStyle, color: "#a5d6a7" }}>
|
||||
{(result.path_coherence_score * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{result.bottleneck_node && (
|
||||
<div style={metricCardStyle}>
|
||||
<div style={metricLabelStyle}>Bottleneck</div>
|
||||
<div
|
||||
style={{
|
||||
...metricValueStyle,
|
||||
color: "#e3b341",
|
||||
fontSize: 11,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
title={result.bottleneck_node}
|
||||
>
|
||||
{getNodeLabel(result.bottleneck_node)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
|
||||
{/* interpretation */}
|
||||
{result.interpretation && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
background: "rgba(88,166,255,0.06)",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(88,166,255,0.14)",
|
||||
color: "#a0b4cc",
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{result.interpretation}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Path Flow Visualizer ──────────────────────────────────────── */
|
||||
|
||||
function getNodeLabel(nodeId: string): string {
|
||||
if (!graph.hasNode(nodeId)) return nodeId;
|
||||
const attrs = graph.getNodeAttributes(nodeId) as { label?: string; content?: string };
|
||||
return String(attrs.label ?? attrs.content ?? nodeId);
|
||||
}
|
||||
|
||||
function getEdgeLabelBetween(sourceId: string, targetId: string, edgeIds?: string[]): string {
|
||||
// Try to find the specific edge from edgeIds first
|
||||
if (edgeIds) {
|
||||
for (const edgeId of edgeIds) {
|
||||
if (graph.hasEdge(edgeId)) {
|
||||
const [src, tgt] = graph.extremities(edgeId);
|
||||
if ((src === sourceId && tgt === targetId) || (src === targetId && tgt === sourceId)) {
|
||||
const attrs = graph.getEdgeAttributes(edgeId) as { edgeType?: string };
|
||||
return attrs.edgeType ?? "→";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: find any edge between the pair
|
||||
if (graph.hasNode(sourceId) && graph.hasNode(targetId)) {
|
||||
let label = "→";
|
||||
graph.forEachEdge(sourceId, targetId, (_edgeId, attrs) => {
|
||||
const edgeAttrs = attrs as { edgeType?: string };
|
||||
if (edgeAttrs.edgeType) label = edgeAttrs.edgeType;
|
||||
});
|
||||
return label;
|
||||
}
|
||||
return "→";
|
||||
}
|
||||
|
||||
function PathFlowViz({
|
||||
path,
|
||||
edgeIds,
|
||||
totalWeight,
|
||||
bottleneckNodeId,
|
||||
onFocusNode,
|
||||
}: {
|
||||
path: string[];
|
||||
edgeIds?: string[];
|
||||
totalWeight: number;
|
||||
bottleneckNodeId?: string | null;
|
||||
onFocusNode?: (nodeId: string) => void;
|
||||
}) {
|
||||
if (path.length === 0) {
|
||||
return <div style={emptyTextStyle}>No path found between the selected nodes.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Horizontal scrollable chip flow */}
|
||||
<div style={pathFlowContainerStyle}>
|
||||
{path.map((nodeId, index) => {
|
||||
const label = getNodeLabel(nodeId);
|
||||
const edgeLabel =
|
||||
index < path.length - 1
|
||||
? getEdgeLabelBetween(nodeId, path[index + 1], edgeIds)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div key={`${nodeId}-${index}`} style={{ display: "contents" }}>
|
||||
{/* Node chip */}
|
||||
<button
|
||||
onClick={() => onFocusNode?.(nodeId)}
|
||||
title={nodeId === bottleneckNodeId ? `Bottleneck: ${nodeId}` : `Focus: ${nodeId}`}
|
||||
style={{
|
||||
...pathNodeChipStyle,
|
||||
cursor: onFocusNode ? "pointer" : "default",
|
||||
...(nodeId === bottleneckNodeId
|
||||
? { border: "1px solid rgba(227,179,65,0.5)", background: "rgba(227,179,65,0.12)" }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<span style={pathNodeIndexStyle}>{index + 1}</span>
|
||||
<span style={{ maxWidth: 120, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Edge connector */}
|
||||
{edgeLabel !== null ? (
|
||||
<div style={pathEdgeConnectorStyle}>
|
||||
<div style={{ width: 16, height: 1, background: "rgba(88,166,255,0.3)" }} />
|
||||
<span style={pathEdgeLabelStyle}>{edgeLabel}</span>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<div style={{ width: 12, height: 1, background: "rgba(88,166,255,0.3)" }} />
|
||||
<div style={{ width: 0, height: 0, borderTop: "4px solid transparent", borderBottom: "4px solid transparent", borderLeft: "5px solid rgba(88,166,255,0.4)" }} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Weight badge */}
|
||||
<div style={{ marginTop: 8, display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ color: "#6a7f97", fontSize: 11 }}>Total weight:</span>
|
||||
<span style={{ color: "#79c0ff", fontSize: 12, fontWeight: 700 }}>{totalWeight.toFixed(3)}</span>
|
||||
<span style={{ color: "#6a7f97", fontSize: 11 }}>·</span>
|
||||
<span style={{ color: "#6a7f97", fontSize: 11 }}>{path.length} hops</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Main Panel ─────────────────────────────────────────────────── */
|
||||
|
||||
export function GraphInspectorPanel({
|
||||
nodeId,
|
||||
inspectableNodeId,
|
||||
selectedNodeKind = "none",
|
||||
canActivateFocused = false,
|
||||
focusedUnavailableReason = null,
|
||||
predictions,
|
||||
predictionType,
|
||||
onPredictionTypeChange,
|
||||
onRunPredictions,
|
||||
isRunningPredictions = false,
|
||||
pathTargetId,
|
||||
onPathTargetChange,
|
||||
onTracePath,
|
||||
pathResult,
|
||||
onDownloadProvenance,
|
||||
onFocusNode,
|
||||
}: GraphInspectorPanelProps) {
|
||||
if (!nodeId) {
|
||||
return (
|
||||
<div style={{ padding: 32, textAlign: "center", display: "flex", flexDirection: "column", alignItems: "center", gap: 12, marginTop: 32 }}>
|
||||
<div style={{ width: 40, height: 40, borderRadius: "50%", background: "rgba(98, 226, 205, 0.07)", border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<div style={{ width: 14, height: 14, borderRadius: "50%", background: GRAPH_THEME.ui.timeline.playheadSoft }} />
|
||||
</div>
|
||||
<p style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 14, margin: 0, lineHeight: 1.6 }}>
|
||||
Search for a node or click one in the canvas to inspect its properties.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedNodeId = inspectableNodeId && graph.hasNode(inspectableNodeId) ? inspectableNodeId : null;
|
||||
const directlyInspectable = graph.hasNode(nodeId);
|
||||
const effectiveNodeId = directlyInspectable ? nodeId : resolvedNodeId;
|
||||
const actionNodeId = directlyInspectable ? nodeId : resolvedNodeId;
|
||||
const groupedDisplaySelection = selectedNodeKind === "grouped" && !directlyInspectable;
|
||||
|
||||
if (!effectiveNodeId) {
|
||||
return (
|
||||
<aside style={{ padding: 24, display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<div style={{ borderBottom: `1px solid ${GRAPH_THEME.ui.surface.divider}`, paddingBottom: 16 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
|
||||
<span style={{ background: GRAPH_THEME.ui.timeline.playhead, boxShadow: "0 0 10px rgba(98, 226, 205, 0.34)", width: 8, height: 8, borderRadius: "50%" }} />
|
||||
<span style={{ color: GRAPH_THEME.ui.timeline.playhead, fontSize: 12, fontWeight: 700 }}>Selection</span>
|
||||
</div>
|
||||
<h3 style={{ margin: 0, color: GRAPH_THEME.ui.text.strong, fontSize: 20, fontWeight: 700, wordBreak: "break-word" }}>
|
||||
{nodeId}
|
||||
</h3>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 12, marginTop: 6, fontFamily: "monospace", wordBreak: "break-all" }}>{nodeId}</div>
|
||||
</div>
|
||||
<div style={groupedSelectionNoticeStyle}>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.strong, fontWeight: 600, marginBottom: 6 }}>Selected item is not directly inspectable in the current graph.</div>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, lineHeight: 1.6 }}>
|
||||
{canActivateFocused
|
||||
? "Activate Focused mode to resolve this grouped selection to its canonical node."
|
||||
: (focusedUnavailableReason ?? "Focused mode is unavailable for the current selection.")}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
const attributes = graph.getNodeAttributes(effectiveNodeId) as {
|
||||
color?: string;
|
||||
content?: string;
|
||||
label?: string;
|
||||
nodeType?: string;
|
||||
valid_from?: string | null;
|
||||
valid_until?: string | null;
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
const properties = attributes?.properties ?? {};
|
||||
const attribution = sourceAttribution(properties);
|
||||
const accentColor = attributes?.color || "#58a6ff";
|
||||
const propertyEntries = Object.entries(properties).filter(
|
||||
([key]) =>
|
||||
!["x","y","valid_from","valid_until","content","source","source_url","pmid","pmids","evidence","provenance","confidence"].includes(key),
|
||||
);
|
||||
|
||||
return (
|
||||
<aside style={{ padding: 24, display: "flex", flexDirection: "column", gap: 18 }}>
|
||||
{/* Node identity */}
|
||||
<div style={{ borderBottom: `1px solid ${GRAPH_THEME.ui.surface.divider}`, paddingBottom: 16 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
|
||||
<span style={{ background: accentColor, boxShadow: `0 0 10px ${accentColor}`, width: 8, height: 8, borderRadius: "50%" }} />
|
||||
<span style={{ color: accentColor, fontSize: 12, fontWeight: 700 }}>
|
||||
{groupedDisplaySelection ? "Grouped Selection" : (attributes?.nodeType || "Entity")}
|
||||
</span>
|
||||
</div>
|
||||
<h3 style={{ margin: 0, color: GRAPH_THEME.ui.text.strong, fontSize: 20, fontWeight: 700, wordBreak: "break-word" }}>
|
||||
{String(attributes?.label ?? effectiveNodeId)}
|
||||
</h3>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 12, marginTop: 6, fontFamily: "monospace", wordBreak: "break-all" }}>
|
||||
{groupedDisplaySelection ? nodeId : effectiveNodeId}
|
||||
</div>
|
||||
{groupedDisplaySelection ? (
|
||||
<div style={groupedSelectionNoticeStyle}>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.strong, fontWeight: 600, marginBottom: 6 }}>This grouped item stays display-level until you explicitly enter Focused mode.</div>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, lineHeight: 1.6 }}>
|
||||
{canActivateFocused
|
||||
? `Canonical node available: ${effectiveNodeId}`
|
||||
: (focusedUnavailableReason ?? "Focused mode is unavailable for the current selection.")}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
|
||||
{attributes?.valid_from || attributes?.valid_until ? (
|
||||
<span style={subtleChipStyle}>temporal</span>
|
||||
) : null}
|
||||
{attribution.length ? <span style={subtleChipStyle}>{attribution.length} source fields</span> : null}
|
||||
{predictions.length ? <span style={subtleChipStyle}>{predictions.length} candidate links</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Temporal bounds */}
|
||||
{(attributes?.valid_from || attributes?.valid_until) ? (
|
||||
<div style={{ padding: "10px 12px", background: "rgba(233, 196, 122, 0.075)", border: "1px solid rgba(233, 196, 122, 0.22)", borderRadius: 8, fontSize: 12, color: GRAPH_THEME.palette.accent.selected, fontFamily: "monospace" }}>
|
||||
{attributes?.valid_from ? <div>from: {attributes.valid_from}</div> : null}
|
||||
{attributes?.valid_until ? <div>until: {attributes.valid_until}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Actions */}
|
||||
<section style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>Actions</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<button
|
||||
style={{ ...actionButtonStyle, width: "100%", justifyContent: "center", opacity: isRunningPredictions ? 0.7 : 1 }}
|
||||
onClick={onRunPredictions}
|
||||
disabled={isRunningPredictions || !actionNodeId}
|
||||
>
|
||||
{isRunningPredictions ? (
|
||||
<Loader2 size={14} className="animate-spin" style={{ marginRight: 6 }} />
|
||||
) : null}
|
||||
{isRunningPredictions ? "Running…" : "Run Link Prediction"}
|
||||
</button>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<button style={secondaryActionButtonStyle} onClick={() => onDownloadProvenance("json")} disabled={!actionNodeId}>
|
||||
Provenance JSON
|
||||
</button>
|
||||
<button style={secondaryActionButtonStyle} onClick={() => onDownloadProvenance("markdown")} disabled={!actionNodeId}>
|
||||
Provenance MD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
value={predictionType}
|
||||
onChange={(event) => onPredictionTypeChange(event.target.value)}
|
||||
placeholder="Optional candidate type filter, e.g. disease"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Trace Path */}
|
||||
<section style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>Trace Path</div>
|
||||
<input
|
||||
value={pathTargetId}
|
||||
onChange={(event) => onPathTargetChange(event.target.value)}
|
||||
placeholder="Target node ID"
|
||||
style={inputStyle}
|
||||
/>
|
||||
<button style={actionButtonStyle} onClick={onTracePath} disabled={!actionNodeId}>Trace Causal Path</button>
|
||||
|
||||
{pathResult?.path?.length ? (
|
||||
<>
|
||||
<PathFlowViz
|
||||
path={pathResult.path}
|
||||
edgeIds={pathResult.edge_ids}
|
||||
totalWeight={pathResult.total_weight}
|
||||
bottleneckNodeId={pathResult.bottleneck_node}
|
||||
onFocusNode={onFocusNode}
|
||||
/>
|
||||
<PathDistanceIntelPanel result={pathResult} />
|
||||
</>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>
|
||||
Choose a target or click a candidate prediction to prepare a path trace.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Candidate Links */}
|
||||
<details className="node-panel-collapse" open={predictions.length > 0}>
|
||||
<summary className="node-panel-summary">Candidate Links</summary>
|
||||
<div className="node-panel-body">
|
||||
{predictions.length > 0 ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{predictions.map((prediction) => (
|
||||
<button
|
||||
key={`${prediction.target}-${prediction.type}`}
|
||||
style={predictionCardStyle}
|
||||
onClick={() => onPathTargetChange(prediction.target)}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
|
||||
<div>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.strong, fontWeight: 600 }}>{prediction.label || prediction.target}</div>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.muted, fontSize: 12 }}>{prediction.type}</div>
|
||||
</div>
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<div style={{
|
||||
padding: "2px 7px",
|
||||
borderRadius: 999,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
background: GRAPH_THEME.ui.timeline.playheadSoft,
|
||||
border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
|
||||
color: GRAPH_THEME.ui.timeline.playhead,
|
||||
}}>
|
||||
{(prediction.score * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : isRunningPredictions ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, padding: 8, color: "#8b949e", fontSize: 12 }}>
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
<span>Computing candidate links…</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>Run link prediction to surface likely next-hop relationships.</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Source Attribution */}
|
||||
<details className="node-panel-collapse">
|
||||
<summary className="node-panel-summary">Source Attribution</summary>
|
||||
<div className="node-panel-body">
|
||||
{attribution.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{attribution.map(({ key, value }) => (
|
||||
<div key={key} style={propertyCardStyle}>
|
||||
<div style={{ color: GRAPH_THEME.ui.timeline.playhead, fontSize: 11, marginBottom: 4 }}>{key}</div>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, wordBreak: "break-word" }}>
|
||||
{typeof value === "object" ? JSON.stringify(value) : String(value)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>No explicit attribution metadata was found on this node.</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Properties */}
|
||||
<details className="node-panel-collapse">
|
||||
<summary className="node-panel-summary">Properties</summary>
|
||||
<div className="node-panel-body">
|
||||
{propertyEntries.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{propertyEntries.map(([key, value]) => (
|
||||
<div key={key} style={propertyCardStyle}>
|
||||
<div style={{ color: GRAPH_THEME.ui.timeline.playhead, fontSize: 11, marginBottom: 4 }}>{key}</div>
|
||||
<div style={{ color: GRAPH_THEME.ui.text.body, fontSize: 13, wordBreak: "break-word" }}>
|
||||
{typeof value === "object" ? JSON.stringify(value) : String(value)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>No additional properties are attached to this node.</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── styles ─────────────────────────────────────────────────────── */
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
background: GRAPH_THEME.ui.control.inputBg,
|
||||
border: `1px solid ${GRAPH_THEME.ui.control.inputBorder}`,
|
||||
color: GRAPH_THEME.ui.text.strong,
|
||||
borderRadius: 12,
|
||||
padding: "11px 13px",
|
||||
fontSize: 13,
|
||||
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.035)",
|
||||
};
|
||||
|
||||
const groupedSelectionNoticeStyle: CSSProperties = {
|
||||
marginTop: 12,
|
||||
padding: "10px 12px",
|
||||
background: "rgba(98, 226, 205, 0.07)",
|
||||
border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
|
||||
borderRadius: 12,
|
||||
};
|
||||
|
||||
const actionButtonStyle: CSSProperties = {
|
||||
background: GRAPH_THEME.ui.control.primaryBg,
|
||||
color: GRAPH_THEME.ui.control.primaryText,
|
||||
border: `1px solid ${GRAPH_THEME.ui.control.primaryBorder}`,
|
||||
borderRadius: 12,
|
||||
padding: "9px 12px",
|
||||
cursor: "pointer",
|
||||
fontWeight: 700,
|
||||
fontSize: 12,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.07), 0 10px 24px rgba(0,0,0,0.18)",
|
||||
};
|
||||
|
||||
const secondaryActionButtonStyle: CSSProperties = {
|
||||
...actionButtonStyle,
|
||||
background: GRAPH_THEME.ui.control.defaultBg,
|
||||
border: `1px solid ${GRAPH_THEME.ui.control.defaultBorder}`,
|
||||
color: GRAPH_THEME.ui.control.defaultText,
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const predictionCardStyle: CSSProperties = {
|
||||
textAlign: "left",
|
||||
padding: "10px 12px",
|
||||
background: "rgba(255, 255, 255, 0.035)",
|
||||
border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
||||
borderRadius: 10,
|
||||
cursor: "pointer",
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
const propertyCardStyle: CSSProperties = {
|
||||
background: "rgba(255, 255, 255, 0.028)",
|
||||
padding: "10px 12px",
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
||||
};
|
||||
|
||||
const emptyTextStyle: CSSProperties = {
|
||||
color: GRAPH_THEME.ui.text.muted,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
};
|
||||
|
||||
const subtleChipStyle: CSSProperties = {
|
||||
background: "rgba(255, 255, 255, 0.04)",
|
||||
color: GRAPH_THEME.ui.text.body,
|
||||
padding: "4px 8px",
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
||||
};
|
||||
|
||||
const sectionStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
padding: 14,
|
||||
background: GRAPH_THEME.ui.surface.cardSubtle,
|
||||
border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
||||
borderRadius: 14,
|
||||
};
|
||||
|
||||
const sectionTitleStyle: CSSProperties = {
|
||||
color: GRAPH_THEME.ui.text.muted,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
};
|
||||
|
||||
const pathFlowContainerStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0,
|
||||
flexWrap: "wrap",
|
||||
rowGap: 8,
|
||||
};
|
||||
|
||||
const pathNodeChipStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "5px 10px",
|
||||
borderRadius: 999,
|
||||
background: "rgba(98, 226, 205, 0.08)",
|
||||
border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
|
||||
color: GRAPH_THEME.ui.text.strong,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
maxWidth: 160,
|
||||
};
|
||||
|
||||
const pathNodeIndexStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: "50%",
|
||||
background: GRAPH_THEME.ui.timeline.playheadSoft,
|
||||
color: GRAPH_THEME.ui.timeline.playhead,
|
||||
fontSize: 9,
|
||||
fontWeight: 800,
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const pathEdgeConnectorStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const pathEdgeLabelStyle: CSSProperties = {
|
||||
fontSize: 9,
|
||||
fontWeight: 700,
|
||||
color: GRAPH_THEME.ui.text.subtle,
|
||||
letterSpacing: "0.04em",
|
||||
textTransform: "uppercase",
|
||||
maxWidth: 70,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
const metricCardStyle: CSSProperties = {
|
||||
background: "rgba(0,0,0,0.18)",
|
||||
borderRadius: 8,
|
||||
padding: "8px 10px",
|
||||
border: "1px solid rgba(255,255,255,0.05)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
};
|
||||
|
||||
const metricLabelStyle: CSSProperties = {
|
||||
color: "rgba(88,166,255,0.65)",
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.06em",
|
||||
textTransform: "uppercase",
|
||||
};
|
||||
|
||||
const metricValueStyle: CSSProperties = {
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: "#e6edf3",
|
||||
};
|
||||
|
||||
const metricBarTrackStyle: CSSProperties = {
|
||||
height: 3,
|
||||
borderRadius: 999,
|
||||
background: "rgba(255,255,255,0.07)",
|
||||
overflow: "hidden",
|
||||
marginTop: 4,
|
||||
};
|
||||
|
||||
const metricBarFillStyle: CSSProperties = {
|
||||
height: "100%",
|
||||
borderRadius: 999,
|
||||
transition: "width 300ms ease",
|
||||
};
|
||||
@@ -0,0 +1,315 @@
|
||||
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
||||
|
||||
import { GRAPH_THEME, withAlpha } from "./graphTheme";
|
||||
import { GRAPH_LOAD_STAGE_SEQUENCE, createGraphLoadProgress, getGraphLoadStageLabel } from "./graphLoading";
|
||||
import type { GraphLoadProgress } from "./types";
|
||||
|
||||
const LOADING_OVERLAY_CSS = `
|
||||
.graph-stage-loader {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
opacity: 1;
|
||||
transition: opacity 220ms ease, transform 220ms ease;
|
||||
}
|
||||
.graph-stage-loader[data-exiting="true"] {
|
||||
opacity: 0;
|
||||
transform: scale(0.985);
|
||||
}
|
||||
.graph-stage-loader-card {
|
||||
width: min(540px, calc(100% - 48px));
|
||||
border-radius: 24px;
|
||||
padding: 20px 20px 18px;
|
||||
border: 1px solid rgba(127, 208, 255, 0.18);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(242, 182, 109, 0.12), transparent 28%),
|
||||
radial-gradient(circle at top left, rgba(127, 208, 255, 0.14), transparent 30%),
|
||||
linear-gradient(145deg, rgba(7, 17, 31, 0.94), rgba(12, 25, 43, 0.82));
|
||||
box-shadow: 0 26px 90px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255,255,255,0.05);
|
||||
backdrop-filter: blur(18px) saturate(1.08);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(1.08);
|
||||
}
|
||||
.graph-stage-loader-card[data-live="true"] {
|
||||
width: min(500px, calc(100% - 56px));
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(242, 182, 109, 0.08), transparent 26%),
|
||||
radial-gradient(circle at top left, rgba(127, 208, 255, 0.12), transparent 28%),
|
||||
linear-gradient(145deg, rgba(7, 17, 31, 0.84), rgba(11, 24, 40, 0.72));
|
||||
box-shadow: 0 18px 54px rgba(0, 0, 0, 0.26), inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
}
|
||||
.graph-stage-loader-beacon {
|
||||
position: relative;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, rgba(127, 208, 255, 0.98), rgba(242, 182, 109, 0.94));
|
||||
box-shadow: 0 0 18px rgba(127, 208, 255, 0.4);
|
||||
}
|
||||
.graph-stage-loader-beacon::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -7px;
|
||||
border-radius: inherit;
|
||||
border: 1px solid rgba(127, 208, 255, 0.18);
|
||||
animation: graph-loader-beacon 1.9s ease-out infinite;
|
||||
}
|
||||
.graph-stage-loader-track {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.graph-stage-loader-step {
|
||||
border-radius: 999px;
|
||||
padding: 7px 0;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid rgba(127, 208, 255, 0.08);
|
||||
color: rgba(143, 168, 198, 0.72);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
.graph-stage-loader-step[data-state="done"] {
|
||||
color: rgba(214, 232, 250, 0.92);
|
||||
border-color: rgba(127, 208, 255, 0.18);
|
||||
background: rgba(89, 155, 220, 0.14);
|
||||
}
|
||||
.graph-stage-loader-step[data-state="active"] {
|
||||
color: #eff7ff;
|
||||
border-color: rgba(242, 182, 109, 0.24);
|
||||
background: linear-gradient(135deg, rgba(49, 108, 172, 0.28), rgba(242, 182, 109, 0.16));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.graph-stage-loader-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(127, 208, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.graph-stage-loader-bar-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, rgba(74, 163, 255, 0.9), rgba(127, 208, 255, 0.96), rgba(242, 182, 109, 0.92));
|
||||
box-shadow: 0 0 30px rgba(74, 163, 255, 0.28);
|
||||
transition: width 220ms ease;
|
||||
}
|
||||
.graph-stage-loader-bar-indeterminate::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
width: 34%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, rgba(74, 163, 255, 0), rgba(127, 208, 255, 0.94), rgba(242, 182, 109, 0.82), rgba(74, 163, 255, 0));
|
||||
box-shadow: 0 0 26px rgba(127, 208, 255, 0.18);
|
||||
animation: graph-loader-sweep 1.5s cubic-bezier(0.22, 1, 0.36, 1) infinite;
|
||||
}
|
||||
@keyframes graph-loader-beacon {
|
||||
0% { transform: scale(0.72); opacity: 0.6; }
|
||||
100% { transform: scale(1.44); opacity: 0; }
|
||||
}
|
||||
@keyframes graph-loader-sweep {
|
||||
0% { transform: translateX(-120%); }
|
||||
100% { transform: translateX(360%); }
|
||||
}
|
||||
`;
|
||||
|
||||
function formatLayoutSource(source: GraphLoadProgress["layoutSource"]) {
|
||||
switch (source) {
|
||||
case "provided":
|
||||
return "Persisted layout";
|
||||
case "carried":
|
||||
return "Preserved layout";
|
||||
case "runtime":
|
||||
return "Runtime layout";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatLayoutState(state: GraphLoadProgress["layoutState"]) {
|
||||
switch (state) {
|
||||
case "bootstrapping":
|
||||
return "Bootstrapping";
|
||||
case "running":
|
||||
return "Settling";
|
||||
case "interactive":
|
||||
return "Interactive";
|
||||
case "stabilized":
|
||||
return "Stable";
|
||||
case "failed":
|
||||
return "Fallback";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const loadingMetricStyle = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "7px 10px",
|
||||
borderRadius: 999,
|
||||
border: "1px solid rgba(127, 208, 255, 0.12)",
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
color: "#b8cade",
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
} satisfies CSSProperties;
|
||||
|
||||
export function GraphLoadingOverlay({
|
||||
progress,
|
||||
visible,
|
||||
showGraphBehind,
|
||||
}: {
|
||||
progress: GraphLoadProgress | null;
|
||||
visible: boolean;
|
||||
showGraphBehind: boolean;
|
||||
}) {
|
||||
const [renderVisible, setRenderVisible] = useState(visible);
|
||||
const [exiting, setExiting] = useState(false);
|
||||
const [displayProgress, setDisplayProgress] = useState<GraphLoadProgress>(
|
||||
progress ?? createGraphLoadProgress({
|
||||
phase: "bootstrapping",
|
||||
message: "Preparing graph session",
|
||||
progressKind: "indeterminate",
|
||||
}),
|
||||
);
|
||||
const exitTimerRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress) {
|
||||
setDisplayProgress(progress);
|
||||
}
|
||||
}, [progress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (exitTimerRef.current !== null) {
|
||||
window.clearTimeout(exitTimerRef.current);
|
||||
exitTimerRef.current = null;
|
||||
}
|
||||
setRenderVisible(true);
|
||||
setExiting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!renderVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
setExiting(true);
|
||||
exitTimerRef.current = window.setTimeout(() => {
|
||||
setRenderVisible(false);
|
||||
setExiting(false);
|
||||
exitTimerRef.current = null;
|
||||
}, 220);
|
||||
|
||||
return () => {
|
||||
if (exitTimerRef.current !== null) {
|
||||
window.clearTimeout(exitTimerRef.current);
|
||||
exitTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [renderVisible, visible]);
|
||||
|
||||
if (!renderVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeProgress = progress ?? displayProgress;
|
||||
const isLiveStage = activeProgress.phase === "stabilizing_layout" || activeProgress.showGraphBehind || showGraphBehind;
|
||||
const overlayBackground = isLiveStage
|
||||
? "linear-gradient(180deg, rgba(1,4,9,0.04), rgba(1,4,9,0.18))"
|
||||
: "linear-gradient(180deg, rgba(1,4,9,0.22), rgba(1,4,9,0.5))";
|
||||
const determinateRatio = activeProgress.progressKind === "determinate" && activeProgress.total
|
||||
? Math.max(0.05, Math.min(activeProgress.loaded ?? 0, activeProgress.total) / Math.max(activeProgress.total, 1))
|
||||
: null;
|
||||
const layoutSource = formatLayoutSource(activeProgress.layoutSource);
|
||||
const layoutState = formatLayoutState(activeProgress.layoutState);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="graph-stage-loader"
|
||||
data-exiting={exiting}
|
||||
style={{ background: overlayBackground }}
|
||||
>
|
||||
<style>{LOADING_OVERLAY_CSS}</style>
|
||||
<div className="graph-stage-loader-card" data-live={isLiveStage}>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 14, marginBottom: 14 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ color: "#ffffff", fontSize: 20, fontWeight: 700, letterSpacing: "-0.03em", marginBottom: 6 }}>
|
||||
{activeProgress.title}
|
||||
</div>
|
||||
<div style={{ color: "#8fa8c6", fontSize: 13, lineHeight: 1.5 }}>
|
||||
{activeProgress.message}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
|
||||
<div className="graph-stage-loader-beacon" aria-hidden="true" />
|
||||
<div style={{ color: "#d7e9fb", fontSize: 11, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase" }}>
|
||||
Stage {activeProgress.stageIndex ?? 1}/{activeProgress.stageCount ?? GRAPH_LOAD_STAGE_SEQUENCE.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="graph-stage-loader-track" style={{ marginBottom: 14 }}>
|
||||
{GRAPH_LOAD_STAGE_SEQUENCE.map((phase, index) => {
|
||||
const current = activeProgress.stageIndex ?? 1;
|
||||
const state = index + 1 < current ? "done" : index + 1 === current ? "active" : "upcoming";
|
||||
return (
|
||||
<div key={phase} className="graph-stage-loader-step" data-state={state}>
|
||||
{getGraphLoadStageLabel(phase)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, alignItems: "baseline", marginBottom: 8 }}>
|
||||
<div style={{ color: "#dce9f6", fontSize: 12, fontWeight: 600 }}>
|
||||
{activeProgress.progressKind === "determinate" && activeProgress.total
|
||||
? `${(activeProgress.loaded ?? 0).toLocaleString()} / ${activeProgress.total.toLocaleString()} in current stage`
|
||||
: "Working through this stage"}
|
||||
</div>
|
||||
<div style={{ color: "#90a8c5", fontSize: 11, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase" }}>
|
||||
{activeProgress.progressKind === "determinate" && determinateRatio !== null
|
||||
? `${Math.round(determinateRatio * 100)}%`
|
||||
: "Live"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`graph-stage-loader-bar ${activeProgress.progressKind === "indeterminate" ? "graph-stage-loader-bar-indeterminate" : ""}`}>
|
||||
{activeProgress.progressKind === "determinate" && determinateRatio !== null ? (
|
||||
<span className="graph-stage-loader-bar-fill" style={{ width: `${Math.round(determinateRatio * 100)}%` }} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginTop: 14 }}>
|
||||
<span style={loadingMetricStyle}>
|
||||
{activeProgress.nodesLoaded.toLocaleString()}
|
||||
{activeProgress.nodesTotal ? ` / ${activeProgress.nodesTotal.toLocaleString()}` : ""} nodes
|
||||
</span>
|
||||
<span style={loadingMetricStyle}>
|
||||
{activeProgress.edgesLoaded.toLocaleString()}
|
||||
{activeProgress.edgesTotal ? ` / ${activeProgress.edgesTotal.toLocaleString()}` : ""} relationships
|
||||
</span>
|
||||
{layoutSource ? (
|
||||
<span style={{ ...loadingMetricStyle, color: "#a9ddff", borderColor: withAlpha(GRAPH_THEME.palette.accent.hovered, 0.22) }}>
|
||||
{layoutSource}
|
||||
{layoutState ? ` · ${layoutState}` : ""}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { batchMergeEdges, batchMergeNodes, clearGraph, graph, type EdgeAttributes, type NodeAttributes } from "../../store/graphStore";
|
||||
import { SigmaSceneAdapter } from "./SigmaSceneAdapter";
|
||||
import { createGraphLoadProgress } from "./graphLoading";
|
||||
import { resolveDisplayGraph } from "./graphSceneState";
|
||||
import {
|
||||
chooseColorAccessor,
|
||||
colorForNodeKey,
|
||||
computeDegreeMap,
|
||||
computeEdgeSize,
|
||||
computeNodeSize,
|
||||
computePageRank,
|
||||
deterministicPosition,
|
||||
} from "./graphAnalytics";
|
||||
import { GRAPH_THEME } from "./graphConfig";
|
||||
import type { GraphSceneHandle } from "./scene";
|
||||
import type {
|
||||
GraphDataSnapshot,
|
||||
GraphEffectsState,
|
||||
GraphLayoutSource,
|
||||
GraphLayoutStatus,
|
||||
GraphLoadProgress,
|
||||
GraphPath,
|
||||
GraphSelectedNodeState,
|
||||
GraphStageHandle,
|
||||
GraphViewMode,
|
||||
} from "./types";
|
||||
|
||||
const STAGE_EFFECTS_STATE: GraphEffectsState = {
|
||||
pathPulseEnabled: false,
|
||||
pathFlowEnabled: false,
|
||||
lensEnabled: false,
|
||||
temporalEmphasisEnabled: false,
|
||||
semanticRegionsEnabled: false,
|
||||
contoursEnabled: false,
|
||||
pathfindingEnabled: false,
|
||||
communitiesEnabled: false,
|
||||
centralityEnabled: false,
|
||||
legendEnabled: false,
|
||||
diagnosticsEnabled: false,
|
||||
lensMode: "neighborhood",
|
||||
effectQuality: "bounded",
|
||||
};
|
||||
const EMPTY_PATH: string[] = [];
|
||||
|
||||
const socketProtocol = () => (window.location.protocol === "https:" ? "wss:" : "ws:");
|
||||
|
||||
function yieldToMain(): Promise<void> {
|
||||
if ("scheduler" in window && typeof (window as Window & { scheduler?: { yield?: () => Promise<void> } }).scheduler?.yield === "function") {
|
||||
return (window as Window & { scheduler: { yield: () => Promise<void> } }).scheduler.yield();
|
||||
}
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
function buildSelectedNodeState(nodeId: string): GraphSelectedNodeState | null {
|
||||
if (!nodeId || !graph.hasNode(nodeId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attributes = graph.getNodeAttributes(nodeId) as NodeAttributes;
|
||||
return {
|
||||
id: nodeId,
|
||||
label: String(attributes.label || nodeId),
|
||||
content: String(attributes.content || attributes.label || nodeId),
|
||||
nodeType: attributes.nodeType || "entity",
|
||||
color: attributes.color,
|
||||
valid_from: attributes.valid_from ?? null,
|
||||
valid_until: attributes.valid_until ?? null,
|
||||
properties: attributes.properties ?? {},
|
||||
neighborCount: graph.neighbors(nodeId).length,
|
||||
visibleNeighborCount: graph.neighbors(nodeId).length,
|
||||
collapsedNeighborCount: 0,
|
||||
isNeighborhoodCollapsed: false,
|
||||
canCollapseNeighborhood: graph.neighbors(nodeId).length > 8,
|
||||
};
|
||||
}
|
||||
|
||||
function hasUsableCoordinate(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
|
||||
interface GraphRuntimeStageProps {
|
||||
snapshot: GraphDataSnapshot | null | undefined;
|
||||
selectedNodeId: string;
|
||||
activePath: GraphPath;
|
||||
onNodeSelect: (nodeId: string) => void;
|
||||
onSelectedNodeStateChange: (state: GraphSelectedNodeState | null) => void;
|
||||
isLayoutRunning: boolean;
|
||||
onLayoutRunningChange: (running: boolean) => void;
|
||||
viewMode: GraphViewMode;
|
||||
temporalTime: Date | null;
|
||||
onActiveNodeCountChange: (count: number | null) => void;
|
||||
onProgressChange: (progress: GraphLoadProgress | null) => void;
|
||||
onLayoutStatusChange: (status: GraphLayoutStatus) => void;
|
||||
onRuntimeReady: () => void;
|
||||
}
|
||||
|
||||
export const GraphRuntimeStage = forwardRef<GraphStageHandle, GraphRuntimeStageProps>(
|
||||
function GraphRuntimeStage(
|
||||
{
|
||||
snapshot,
|
||||
selectedNodeId,
|
||||
activePath,
|
||||
onNodeSelect,
|
||||
onSelectedNodeStateChange,
|
||||
isLayoutRunning,
|
||||
onLayoutRunningChange,
|
||||
viewMode,
|
||||
temporalTime,
|
||||
onActiveNodeCountChange,
|
||||
onProgressChange,
|
||||
onLayoutStatusChange,
|
||||
onRuntimeReady,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const sceneRef = useRef<GraphSceneHandle>(null);
|
||||
const prevActiveIdsRef = useRef<Set<string>>(new Set());
|
||||
const [graphVersion, setGraphVersion] = useState(0);
|
||||
const [runtimeLayoutSource, setRuntimeLayoutSource] = useState<GraphLayoutSource>(snapshot?.summary.layoutSource ?? "runtime");
|
||||
const displayResult = useMemo(
|
||||
() => resolveDisplayGraph(selectedNodeId, activePath, EMPTY_PATH, viewMode, { aggregationEnabled: true }),
|
||||
[activePath, graphVersion, selectedNodeId, viewMode],
|
||||
);
|
||||
|
||||
const stageSignature = useMemo(() => (snapshot ? `${snapshot.fetchedAt}:${snapshot.summary.nodeCount}:${snapshot.summary.edgeCount}` : null), [snapshot]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
fitView: () => sceneRef.current?.fitView(),
|
||||
focusNode: (nodeId: string) => sceneRef.current?.focusNode(nodeId),
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function hydrateSnapshot() {
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
onProgressChange(createGraphLoadProgress({
|
||||
phase: "computing_styling",
|
||||
progressKind: "indeterminate",
|
||||
nodesLoaded: snapshot.summary.nodeCount,
|
||||
nodesTotal: snapshot.summary.nodeCount,
|
||||
edgesLoaded: snapshot.summary.edgeCount,
|
||||
edgesTotal: snapshot.summary.edgeCount,
|
||||
message: "Computing runtime graph styling",
|
||||
showGraphBehind: false,
|
||||
}));
|
||||
|
||||
const degreeByNode = computeDegreeMap(snapshot.nodes, snapshot.edges);
|
||||
const pageRankByNode = computePageRank(snapshot.nodes, snapshot.edges);
|
||||
const nodeIndexById = new Map(snapshot.nodes.map((node, index) => [node.id, index]));
|
||||
const previousPositions = new Map<string, { x: number; y: number }>();
|
||||
|
||||
graph.forEachNode((nodeId, attributes) => {
|
||||
const raw = attributes as Partial<NodeAttributes>;
|
||||
const x = Number(raw.x);
|
||||
const y = Number(raw.y);
|
||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||
previousPositions.set(nodeId, { x, y });
|
||||
}
|
||||
});
|
||||
|
||||
let explicitCoordinateCount = 0;
|
||||
let carriedCoordinateCount = 0;
|
||||
const draftAttributes = snapshot.nodes.map((node) => {
|
||||
const previousPosition = previousPositions.get(node.id);
|
||||
const position = hasUsableCoordinate(node.x) && hasUsableCoordinate(node.y)
|
||||
? { x: node.x, y: node.y }
|
||||
: previousPosition
|
||||
? previousPosition
|
||||
: deterministicPosition(node.id, nodeIndexById.get(node.id) ?? 0, snapshot.nodes.length);
|
||||
|
||||
if (hasUsableCoordinate(node.x) && hasUsableCoordinate(node.y)) {
|
||||
explicitCoordinateCount += 1;
|
||||
} else if (previousPosition) {
|
||||
carriedCoordinateCount += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
attributes: {
|
||||
label: node.content || node.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
nodeType: node.type,
|
||||
content: node.content,
|
||||
valid_from: node.valid_from,
|
||||
valid_until: node.valid_until,
|
||||
properties: node.properties,
|
||||
} as NodeAttributes,
|
||||
};
|
||||
});
|
||||
|
||||
const layoutSource: GraphLayoutSource = explicitCoordinateCount > 0
|
||||
? "provided"
|
||||
: carriedCoordinateCount > 0
|
||||
? "carried"
|
||||
: "runtime";
|
||||
const hasCoordinates = explicitCoordinateCount > 0 || carriedCoordinateCount > 0;
|
||||
setRuntimeLayoutSource(layoutSource);
|
||||
|
||||
const colorAccessor = chooseColorAccessor(draftAttributes);
|
||||
await yieldToMain();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onProgressChange(createGraphLoadProgress({
|
||||
phase: "hydrating_scene",
|
||||
progressKind: "indeterminate",
|
||||
nodesLoaded: snapshot.summary.nodeCount,
|
||||
nodesTotal: snapshot.summary.nodeCount,
|
||||
edgesLoaded: snapshot.summary.edgeCount,
|
||||
edgesTotal: snapshot.summary.edgeCount,
|
||||
message: "Hydrating graph scene and renderer",
|
||||
showGraphBehind: false,
|
||||
}));
|
||||
|
||||
const nodesToMerge = draftAttributes.map(({ id, attributes }) => {
|
||||
const colorKey = colorAccessor(id, attributes);
|
||||
const baseColor = colorForNodeKey(colorKey);
|
||||
const dynamicSize = computeNodeSize(id, degreeByNode, pageRankByNode);
|
||||
return {
|
||||
id,
|
||||
attributes: {
|
||||
...attributes,
|
||||
color: baseColor,
|
||||
baseColor,
|
||||
size: dynamicSize,
|
||||
baseSize: dynamicSize,
|
||||
degree: degreeByNode.get(id) ?? 0,
|
||||
pageRank: pageRankByNode.get(id) ?? 0,
|
||||
glowColor: baseColor,
|
||||
borderColor: GRAPH_THEME.nodes.border,
|
||||
borderSize: 1,
|
||||
} as NodeAttributes,
|
||||
};
|
||||
});
|
||||
|
||||
const edgesToMerge = snapshot.edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
familyId: edge.familyId,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
attributes: {
|
||||
edgeId: edge.id,
|
||||
familyId: edge.familyId,
|
||||
sourceId: edge.source,
|
||||
targetId: edge.target,
|
||||
weight: edge.weight,
|
||||
edgeType: edge.type,
|
||||
properties: edge.properties,
|
||||
size: computeEdgeSize(edge.weight),
|
||||
baseSize: computeEdgeSize(edge.weight),
|
||||
color: GRAPH_THEME.edges.baseColor,
|
||||
baseColor: GRAPH_THEME.edges.baseColor,
|
||||
} as EdgeAttributes,
|
||||
}));
|
||||
|
||||
clearGraph();
|
||||
batchMergeNodes(nodesToMerge);
|
||||
batchMergeEdges(edgesToMerge);
|
||||
prevActiveIdsRef.current = new Set(snapshot.nodes.map((node) => node.id));
|
||||
|
||||
await yieldToMain();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onLayoutStatusChange({
|
||||
state: layoutSource === "runtime" ? "bootstrapping" : "interactive",
|
||||
source: layoutSource,
|
||||
hasCoordinates,
|
||||
layoutReady: layoutSource !== "runtime",
|
||||
displacement: null,
|
||||
elapsedMs: 0,
|
||||
stableSamples: 0,
|
||||
});
|
||||
|
||||
onLayoutRunningChange(layoutSource === "runtime");
|
||||
if (selectedNodeId) {
|
||||
sceneRef.current?.focusNode(selectedNodeId);
|
||||
} else {
|
||||
sceneRef.current?.getRuntime()?.requestRender();
|
||||
}
|
||||
setGraphVersion((current) => current + 1);
|
||||
if (layoutSource !== "runtime") {
|
||||
onProgressChange(null);
|
||||
} else {
|
||||
onProgressChange(createGraphLoadProgress({
|
||||
phase: "stabilizing_layout",
|
||||
progressKind: "indeterminate",
|
||||
nodesLoaded: snapshot.summary.nodeCount,
|
||||
nodesTotal: snapshot.summary.nodeCount,
|
||||
edgesLoaded: snapshot.summary.edgeCount,
|
||||
edgesTotal: snapshot.summary.edgeCount,
|
||||
message: "Settling runtime layout",
|
||||
showGraphBehind: true,
|
||||
layoutSource,
|
||||
layoutState: "bootstrapping",
|
||||
}));
|
||||
}
|
||||
onRuntimeReady();
|
||||
}
|
||||
|
||||
void hydrateSnapshot();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [onLayoutRunningChange, onLayoutStatusChange, onProgressChange, onRuntimeReady, selectedNodeId, snapshot, stageSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedNodeId) {
|
||||
onSelectedNodeStateChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectedNodeStateChange(buildSelectedNodeState(selectedNodeId));
|
||||
}, [graphVersion, onSelectedNodeStateChange, selectedNodeId, viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!snapshot || !temporalTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const applySnapshot = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/temporal/snapshot?at=${encodeURIComponent(temporalTime.toISOString())}`);
|
||||
if (!response.ok || cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: { active_node_ids: string[]; active_node_count: number } = await response.json();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextActiveIds = new Set(data.active_node_ids);
|
||||
requestAnimationFrame(() => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = prevActiveIdsRef.current;
|
||||
previous.forEach((id) => {
|
||||
if (!nextActiveIds.has(id) && graph.hasNode(id)) {
|
||||
graph.setNodeAttribute(id, "hidden", true);
|
||||
}
|
||||
});
|
||||
nextActiveIds.forEach((id) => {
|
||||
if (graph.hasNode(id)) {
|
||||
graph.setNodeAttribute(id, "hidden", false);
|
||||
}
|
||||
});
|
||||
|
||||
prevActiveIdsRef.current = nextActiveIds;
|
||||
onActiveNodeCountChange(data.active_node_count);
|
||||
sceneRef.current?.getRuntime()?.requestRender();
|
||||
});
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
console.error("[GraphRuntimeStage] temporal snapshot failed", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void applySnapshot();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [onActiveNodeCountChange, snapshot, temporalTime]);
|
||||
|
||||
useEffect(() => {
|
||||
const socket = new WebSocket(`${socketProtocol()}//${window.location.host}/ws/graph-updates`);
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === "connection_ack" || message.event !== "graph_mutation") {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventType = message.data?.event_type;
|
||||
const payload = message.data?.payload;
|
||||
if (eventType === "ADD_NODE" && payload?.id) {
|
||||
batchMergeNodes([
|
||||
{
|
||||
id: payload.id,
|
||||
attributes: {
|
||||
label: payload.properties?.content || payload.id,
|
||||
x: Number.isFinite(Number(payload.x ?? payload.properties?.x))
|
||||
? Number(payload.x ?? payload.properties?.x)
|
||||
: deterministicPosition(payload.id, graph.order + 1, Math.max(graph.order + 1, 1)).x,
|
||||
y: Number.isFinite(Number(payload.y ?? payload.properties?.y))
|
||||
? Number(payload.y ?? payload.properties?.y)
|
||||
: deterministicPosition(payload.id, graph.order + 1, Math.max(graph.order + 1, 1)).y,
|
||||
nodeType: payload.type,
|
||||
content: payload.properties?.content || payload.id,
|
||||
valid_from: payload.properties?.valid_from ?? null,
|
||||
valid_until: payload.properties?.valid_until ?? null,
|
||||
properties: payload.properties || {},
|
||||
size: 8,
|
||||
color: colorForNodeKey(`${payload.type || "entity"}:${payload.id}`),
|
||||
baseColor: colorForNodeKey(`${payload.type || "entity"}:${payload.id}`),
|
||||
baseSize: 8,
|
||||
glowColor: colorForNodeKey(`${payload.type || "entity"}:${payload.id}`),
|
||||
borderColor: GRAPH_THEME.nodes.border,
|
||||
borderSize: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (eventType === "ADD_EDGE" && payload?.source_id && payload?.target_id) {
|
||||
batchMergeEdges([
|
||||
{
|
||||
id: String(payload.id),
|
||||
familyId: payload.familyId ? String(payload.familyId) : String(payload.id),
|
||||
source: payload.source_id,
|
||||
target: payload.target_id,
|
||||
attributes: {
|
||||
edgeId: String(payload.id),
|
||||
familyId: payload.familyId ? String(payload.familyId) : String(payload.id),
|
||||
sourceId: payload.source_id,
|
||||
targetId: payload.target_id,
|
||||
weight: Number(payload.weight ?? 1),
|
||||
edgeType: payload.type,
|
||||
properties: payload.properties || {},
|
||||
size: computeEdgeSize(Number(payload.weight ?? 1)),
|
||||
baseSize: computeEdgeSize(Number(payload.weight ?? 1)),
|
||||
color: payload.properties?.inferred ? GRAPH_THEME.edges.pathColor : GRAPH_THEME.edges.baseColor,
|
||||
baseColor: GRAPH_THEME.edges.baseColor,
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
sceneRef.current?.getRuntime()?.requestRender();
|
||||
setGraphVersion((current) => current + 1);
|
||||
} catch (error) {
|
||||
console.error("[GraphRuntimeStage] websocket update failed", error);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SigmaSceneAdapter
|
||||
ref={sceneRef}
|
||||
onNodeSelect={onNodeSelect}
|
||||
graphVersion={graphVersion}
|
||||
graphReady={Boolean(snapshot)}
|
||||
displayGraph={displayResult.graph}
|
||||
displayMeta={displayResult.meta}
|
||||
displayState={displayResult.state}
|
||||
selectedEdgeId=""
|
||||
selectedNodeId={selectedNodeId}
|
||||
focusedNodeId={viewMode === "focused" ? selectedNodeId : ""}
|
||||
activePath={activePath}
|
||||
activePathEdgeIds={EMPTY_PATH}
|
||||
effectsState={STAGE_EFFECTS_STATE}
|
||||
isLayoutRunning={isLayoutRunning}
|
||||
onLayoutRunningChange={onLayoutRunningChange}
|
||||
layoutSource={runtimeLayoutSource}
|
||||
onLayoutStatusChange={onLayoutStatusChange}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,849 @@
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
|
||||
import { GraphLoadingOverlay } from "./GraphLoadingOverlay";
|
||||
import { getGraphLoadTitle } from "./graphLoading";
|
||||
import { useGraphData, useReloadGraphData } from "./useGraphData";
|
||||
import type {
|
||||
ApiNode,
|
||||
GraphLayoutStatus,
|
||||
GraphLoadProgress,
|
||||
GraphPath,
|
||||
GraphSelectedNodeState,
|
||||
GraphStageHandle,
|
||||
GraphViewMode,
|
||||
} from "./types";
|
||||
|
||||
type SearchResult = {
|
||||
node: {
|
||||
id: string;
|
||||
type: string;
|
||||
content: string;
|
||||
properties: Record<string, unknown>;
|
||||
};
|
||||
score: number;
|
||||
};
|
||||
|
||||
type LinkPrediction = {
|
||||
target: string;
|
||||
type: string;
|
||||
label?: string;
|
||||
score: number;
|
||||
};
|
||||
|
||||
type PathResponse = {
|
||||
path: GraphPath;
|
||||
total_weight: number;
|
||||
hop_count: number;
|
||||
distance_band: "direct" | "near" | "mid-range" | "distant";
|
||||
};
|
||||
|
||||
type TemporalBounds = {
|
||||
min?: string | null;
|
||||
max?: string | null;
|
||||
};
|
||||
|
||||
const GraphRuntimeStage = lazy(() =>
|
||||
import("./GraphRuntimeStage").then((module) => ({ default: module.GraphRuntimeStage })),
|
||||
);
|
||||
const TimelinePanel = lazy(() =>
|
||||
import("./TimelinePanel").then((module) => ({ default: module.TimelinePanel })),
|
||||
);
|
||||
|
||||
const HUD_CSS = `
|
||||
.palantir-bg {
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(103, 182, 255, 0.1), transparent 24%),
|
||||
linear-gradient(180deg, #07111d 0%, #02060e 100%);
|
||||
}
|
||||
.palantir-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(88, 166, 255, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(88, 166, 255, 0.04) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
opacity: 0.78;
|
||||
}
|
||||
.palantir-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 34%, rgba(1, 4, 9, 0.88) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.hud-scrollbar::-webkit-scrollbar { width: 6px; }
|
||||
.hud-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||
.hud-scrollbar::-webkit-scrollbar-thumb { background: rgba(88, 166, 255, 0.25); border-radius: 6px; }
|
||||
.graph-shell-top { position: absolute; top: 18px; left: 18px; right: 18px; z-index: 10; display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; pointer-events: none; }
|
||||
.graph-status-card, .graph-command-card {
|
||||
pointer-events: auto;
|
||||
border: 1px solid rgba(132, 197, 255, 0.12);
|
||||
background: linear-gradient(180deg, rgba(7, 16, 29, 0.86), rgba(10, 22, 39, 0.72)), radial-gradient(circle at top, rgba(103, 182, 255, 0.08), transparent 50%);
|
||||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
.graph-status-card { width: min(420px, 38vw); border-radius: 24px; padding: 16px 18px; }
|
||||
.graph-command-card { width: min(620px, 55vw); border-radius: 24px; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.graph-status-label { display: inline-flex; align-items: center; gap: 8px; color: rgba(160, 191, 223, 0.88); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px; }
|
||||
.graph-status-label::before { content: ""; width: 7px; height: 7px; border-radius: 999px; background: linear-gradient(135deg, #8ed3ff, #ffb36a); box-shadow: 0 0 12px rgba(142, 211, 255, 0.5); }
|
||||
.graph-status-title { color: #eef5ff; font-size: 20px; font-weight: 800; letter-spacing: -0.04em; margin-bottom: 6px; }
|
||||
.graph-status-copy { color: #8fa8c6; font-size: 12px; line-height: 1.55; margin-bottom: 14px; max-width: 40ch; }
|
||||
.graph-status-metrics, .graph-command-row, .graph-toggle-cluster, .graph-action-cluster { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.graph-command-row { justify-content: space-between; align-items: center; gap: 10px; }
|
||||
.graph-search-shell { flex: 1; min-width: 260px; display: flex; align-items: center; gap: 10px; padding: 8px 10px 8px 14px; border-radius: 18px; border: 1px solid rgba(132, 197, 255, 0.12); background: rgba(0, 0, 0, 0.18); box-shadow: inset 0 1px 0 rgba(255,255,255,0.03); }
|
||||
.graph-search-shell input { flex: 1; min-width: 0; border: none !important; background: transparent !important; padding: 0 !important; margin: 0 !important; }
|
||||
.graph-search-shell input:focus { outline: none; }
|
||||
.graph-search-results { position: absolute; top: 120px; right: 18px; width: min(420px, calc(100vw - 132px)); max-height: 320px; overflow-y: auto; padding: 12px; border-radius: 20px; border: 1px solid rgba(132, 197, 255, 0.14); background: linear-gradient(180deg, rgba(8, 18, 33, 0.94), rgba(10, 21, 38, 0.86)); box-shadow: 0 18px 50px rgba(0,0,0,0.34); backdrop-filter: blur(18px); pointer-events: auto; z-index: 11; }
|
||||
.graph-search-results-label { color: #6f89ab; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px; }
|
||||
.graph-search-result-card { width: 100%; text-align: left; padding: 12px 14px; border-radius: 16px; border: 1px solid rgba(132, 197, 255, 0.08); background: rgba(255, 255, 255, 0.025); cursor: pointer; transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; }
|
||||
.graph-search-result-card:hover { transform: translateY(-1px); border-color: rgba(132, 197, 255, 0.18); background: rgba(103, 182, 255, 0.08); }
|
||||
.graph-inspector { pointer-events: auto; position: absolute; right: 18px; top: 154px; bottom: 108px; width: 380px; overflow-y: auto; transition: transform 0.34s cubic-bezier(0.16,1,0.3,1), opacity 0.22s ease; border-radius: 28px; border: 1px solid rgba(132, 197, 255, 0.14); background: linear-gradient(180deg, rgba(8, 18, 33, 0.9), rgba(6, 12, 22, 0.88)), radial-gradient(circle at top, rgba(103, 182, 255, 0.08), transparent 40%); box-shadow: -18px 0 48px rgba(0, 0, 0, 0.32), inset 0 1px 0 rgba(255,255,255,0.04); backdrop-filter: blur(20px); }
|
||||
.graph-inspector[data-open='false'] { transform: translateX(calc(100% + 24px)); opacity: 0; }
|
||||
@keyframes sem-loader-pulse {
|
||||
0%, 100% { transform: translateY(0) scale(0.92); opacity: 0.55; }
|
||||
50% { transform: translateY(-4px) scale(1.08); opacity: 1; }
|
||||
}
|
||||
@media (max-width: 1220px) {
|
||||
.graph-shell-top { flex-direction: column; align-items: stretch; }
|
||||
.graph-status-card, .graph-command-card { width: auto; }
|
||||
.graph-search-results { top: 202px; right: 18px; left: 18px; width: auto; }
|
||||
}
|
||||
`;
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [delay, value]);
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
function sourceAttribution(properties: Record<string, unknown>) {
|
||||
const keys = ["source", "source_url", "pmid", "pmids", "evidence", "provenance", "confidence"];
|
||||
return keys
|
||||
.filter((key) => key in properties)
|
||||
.map((key) => ({ key, value: properties[key] }));
|
||||
}
|
||||
|
||||
function toSelectedNodeState(node: ApiNode, neighborCount: number, fallbackColor = "#58a6ff"): GraphSelectedNodeState {
|
||||
return {
|
||||
id: node.id,
|
||||
label: node.content || node.id,
|
||||
content: node.content || node.id,
|
||||
nodeType: node.type,
|
||||
color: fallbackColor,
|
||||
valid_from: node.valid_from ?? null,
|
||||
valid_until: node.valid_until ?? null,
|
||||
properties: node.properties ?? {},
|
||||
neighborCount,
|
||||
visibleNeighborCount: neighborCount,
|
||||
collapsedNeighborCount: 0,
|
||||
isNeighborhoodCollapsed: false,
|
||||
canCollapseNeighborhood: neighborCount > 8,
|
||||
};
|
||||
}
|
||||
|
||||
function TimelineFallback({ min, max }: TemporalBounds) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "90px",
|
||||
borderTop: "1px solid rgba(88, 166, 255, 0.2)",
|
||||
background: "rgba(1, 4, 9, 0.88)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "0 18px",
|
||||
color: "#8fa8c6",
|
||||
fontSize: 12,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span>Temporal scrubber</span>
|
||||
<span>{min || max ? "Preparing timeline runtime..." : "Temporal bounds loading..."}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NodePanel({
|
||||
node,
|
||||
predictions,
|
||||
predictionType,
|
||||
onPredictionTypeChange,
|
||||
onRunPredictions,
|
||||
pathTargetId,
|
||||
onPathTargetChange,
|
||||
onTracePath,
|
||||
pathResult,
|
||||
onDownloadProvenance,
|
||||
}: {
|
||||
node: GraphSelectedNodeState | null;
|
||||
predictions: LinkPrediction[];
|
||||
predictionType: string;
|
||||
onPredictionTypeChange: (value: string) => void;
|
||||
onRunPredictions: () => void;
|
||||
pathTargetId: string;
|
||||
onPathTargetChange: (value: string) => void;
|
||||
onTracePath: () => void;
|
||||
pathResult: PathResponse | null;
|
||||
onDownloadProvenance: (format: "json" | "markdown") => void;
|
||||
}) {
|
||||
if (!node) {
|
||||
return (
|
||||
<div style={{ padding: 32, textAlign: "center" }}>
|
||||
<p style={{ color: "#8b949e", fontSize: 14, margin: 0 }}>
|
||||
Search for a node or click one in the canvas to inspect its properties.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const properties = node.properties ?? {};
|
||||
const attribution = sourceAttribution(properties);
|
||||
const accentColor = node.color || "#58a6ff";
|
||||
const propertyEntries = Object.entries(properties).filter(([key]) => !["x", "y", "valid_from", "valid_until", "content", "source", "source_url", "pmid", "pmids", "evidence", "provenance", "confidence"].includes(key));
|
||||
|
||||
return (
|
||||
<aside style={{ padding: 24, display: "flex", flexDirection: "column", gap: 18 }}>
|
||||
<div style={{ borderBottom: "1px solid rgba(88, 166, 255, 0.14)", paddingBottom: 16 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
|
||||
<span style={{ background: accentColor, boxShadow: `0 0 10px ${accentColor}`, width: 8, height: 8, borderRadius: "50%" }} />
|
||||
<span style={{ color: accentColor, fontSize: 12, fontWeight: 800, textTransform: "uppercase", letterSpacing: "0.08em" }}>{node.nodeType || "Entity"}</span>
|
||||
</div>
|
||||
<h3 style={{ margin: 0, color: "#fff", fontSize: 24, lineHeight: 1, fontWeight: 800, letterSpacing: "-0.04em", wordBreak: "break-word" }}>{node.label}</h3>
|
||||
<div style={{ color: "#8b949e", fontSize: 12, marginTop: 8 }}>{node.id}</div>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
|
||||
{node.valid_from || node.valid_until ? <span style={subtleChipStyle}>temporal</span> : null}
|
||||
<span style={subtleChipStyle}>{node.neighborCount} neighbors</span>
|
||||
{attribution.length ? <span style={subtleChipStyle}>{attribution.length} source fields</span> : null}
|
||||
{predictions.length ? <span style={subtleChipStyle}>{predictions.length} candidate links</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>Actions</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<button style={{ ...actionButtonStyle, width: "100%", justifyContent: "center" }} onClick={onRunPredictions}>Run Link Prediction</button>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<button style={secondaryActionButtonStyle} onClick={() => onDownloadProvenance("json")}>Provenance JSON</button>
|
||||
<button style={secondaryActionButtonStyle} onClick={() => onDownloadProvenance("markdown")}>Provenance MD</button>
|
||||
</div>
|
||||
</div>
|
||||
<input value={predictionType} onChange={(event) => onPredictionTypeChange(event.target.value)} placeholder="Optional candidate type filter, e.g. disease" style={inputStyle} />
|
||||
</section>
|
||||
|
||||
<section style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>Trace Path</div>
|
||||
<input value={pathTargetId} onChange={(event) => onPathTargetChange(event.target.value)} placeholder="Target node ID" style={inputStyle} />
|
||||
<button style={actionButtonStyle} onClick={onTracePath}>Trace Causal Path</button>
|
||||
{pathResult?.path?.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 10 }}>
|
||||
{pathResult.path.map((step, index) => (
|
||||
<div key={`${step}-${index}`} style={pathStepStyle}>{index + 1}. {step}</div>
|
||||
))}
|
||||
<div style={{ color: "#79c0ff", fontSize: 12, marginTop: 4 }}>total weight: {pathResult.total_weight.toFixed(3)}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>Choose a target or click a candidate prediction to prepare a path trace.</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<details style={collapseStyle} open={predictions.length > 0}>
|
||||
<summary style={summaryStyle}>Candidate Links</summary>
|
||||
<div style={{ padding: "0 14px 14px" }}>
|
||||
{predictions.length > 0 ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{predictions.map((prediction) => (
|
||||
<button key={`${prediction.target}-${prediction.type}`} style={predictionCardStyle} onClick={() => onPathTargetChange(prediction.target)}>
|
||||
<div style={{ color: "#fff", fontWeight: 600 }}>{prediction.label || prediction.target}</div>
|
||||
<div style={{ color: "#8b949e", fontSize: 12 }}>{prediction.type}</div>
|
||||
<div style={{ color: "#58a6ff", fontSize: 12, marginTop: 4 }}>confidence {prediction.score.toFixed(3)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>Run link prediction to surface likely next-hop relationships.</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details style={collapseStyle}>
|
||||
<summary style={summaryStyle}>Source Attribution</summary>
|
||||
<div style={{ padding: "0 14px 14px" }}>
|
||||
{attribution.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{attribution.map(({ key, value }) => (
|
||||
<div key={key} style={propertyCardStyle}>
|
||||
<div style={{ color: "rgba(88, 166, 255, 0.7)", fontSize: 11, marginBottom: 4 }}>{key}</div>
|
||||
<div style={{ color: "#e6edf3", fontSize: 13, wordBreak: "break-word" }}>{typeof value === "object" ? JSON.stringify(value) : String(value)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>No explicit attribution metadata was found on this node.</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details style={collapseStyle}>
|
||||
<summary style={summaryStyle}>Properties</summary>
|
||||
<div style={{ padding: "0 14px 14px" }}>
|
||||
{propertyEntries.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{propertyEntries.map(([key, value]) => (
|
||||
<div key={key} style={propertyCardStyle}>
|
||||
<div style={{ color: "rgba(88, 166, 255, 0.7)", fontSize: 11, marginBottom: 4 }}>{key}</div>
|
||||
<div style={{ color: "#e6edf3", fontSize: 13, wordBreak: "break-word" }}>{typeof value === "object" ? JSON.stringify(value) : String(value)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={emptyTextStyle}>No additional properties are attached to this node.</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export function GraphWorkspaceShell() {
|
||||
const [selectedNodeId, setSelectedNodeId] = useState("");
|
||||
const [selectedNodeState, setSelectedNodeState] = useState<GraphSelectedNodeState | null>(null);
|
||||
const [isLayoutRunning, setIsLayoutRunning] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<GraphViewMode>("full");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||
const [searchError, setSearchError] = useState("");
|
||||
const [predictionType, setPredictionType] = useState("");
|
||||
const [predictions, setPredictions] = useState<LinkPrediction[]>([]);
|
||||
const [pathTargetId, setPathTargetId] = useState("");
|
||||
const [pathResult, setPathResult] = useState<PathResponse | null>(null);
|
||||
const [activeNodeCount, setActiveNodeCount] = useState<number | null>(null);
|
||||
const [temporalBounds, setTemporalBounds] = useState<TemporalBounds | null>(null);
|
||||
const [scrubberTime, setScrubberTime] = useState<Date | null>(null);
|
||||
const [loadingProgress, setLoadingProgress] = useState<GraphLoadProgress | null>(null);
|
||||
const [isGraphStageReady, setIsGraphStageReady] = useState(false);
|
||||
const [layoutStatus, setLayoutStatus] = useState<GraphLayoutStatus>({
|
||||
state: "idle",
|
||||
source: "runtime",
|
||||
hasCoordinates: false,
|
||||
layoutReady: false,
|
||||
displacement: null,
|
||||
elapsedMs: 0,
|
||||
stableSamples: 0,
|
||||
});
|
||||
|
||||
const debouncedTime = useDebounce(scrubberTime, 150);
|
||||
const stageRef = useRef<GraphStageHandle>(null);
|
||||
const reload = useReloadGraphData();
|
||||
const { data: snapshot, isLoading, isFetching, isError, error } = useGraphData({ enabled: true, onProgress: setLoadingProgress });
|
||||
|
||||
const handleSelectedNodeStateChange = useCallback((state: GraphSelectedNodeState | null) => {
|
||||
setSelectedNodeState(state);
|
||||
}, []);
|
||||
|
||||
const handleLayoutRunningChange = useCallback((running: boolean) => {
|
||||
setIsLayoutRunning(running);
|
||||
}, []);
|
||||
|
||||
const handleActiveNodeCountChange = useCallback((count: number | null) => {
|
||||
setActiveNodeCount(count);
|
||||
}, []);
|
||||
|
||||
const handleProgressChange = useCallback((progress: GraphLoadProgress | null) => {
|
||||
setLoadingProgress(progress);
|
||||
}, []);
|
||||
|
||||
const handleRuntimeReady = useCallback(() => {
|
||||
setIsGraphStageReady(true);
|
||||
}, []);
|
||||
|
||||
const handleLayoutStatusChange = useCallback((status: GraphLayoutStatus) => {
|
||||
setLayoutStatus(status);
|
||||
if (status.layoutReady) {
|
||||
setLoadingProgress(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot) {
|
||||
setIsGraphStageReady(false);
|
||||
setActiveNodeCount(null);
|
||||
setLayoutStatus({
|
||||
state: snapshot.summary.layoutReady ? "interactive" : "idle",
|
||||
source: snapshot.summary.layoutSource ?? "runtime",
|
||||
hasCoordinates: snapshot.summary.hasCoordinates ?? false,
|
||||
layoutReady: snapshot.summary.layoutReady ?? false,
|
||||
displacement: null,
|
||||
elapsedMs: 0,
|
||||
stableSamples: 0,
|
||||
});
|
||||
}
|
||||
}, [snapshot?.fetchedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const loadBounds = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/temporal/bounds");
|
||||
if (!response.ok || cancelled) return;
|
||||
const data: TemporalBounds = await response.json();
|
||||
if (!cancelled) setTemporalBounds(data);
|
||||
} catch {
|
||||
if (!cancelled) setTemporalBounds(null);
|
||||
}
|
||||
};
|
||||
void loadBounds();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [snapshot?.summary.nodeCount, snapshot?.summary.edgeCount]);
|
||||
|
||||
const neighborCountMap = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
if (!snapshot) return map;
|
||||
for (const node of snapshot.nodes) map.set(node.id, 0);
|
||||
for (const edge of snapshot.edges) {
|
||||
map.set(edge.source, (map.get(edge.source) ?? 0) + 1);
|
||||
map.set(edge.target, (map.get(edge.target) ?? 0) + 1);
|
||||
}
|
||||
return map;
|
||||
}, [snapshot]);
|
||||
|
||||
const visibleSelectedNode = useMemo(() => {
|
||||
if (!selectedNodeId) return null;
|
||||
if (selectedNodeState?.id === selectedNodeId) return selectedNodeState;
|
||||
const snapshotNode = snapshot?.nodes.find((candidate) => candidate.id === selectedNodeId);
|
||||
if (snapshotNode) return toSelectedNodeState(snapshotNode, neighborCountMap.get(snapshotNode.id) ?? 0);
|
||||
const searchNode = searchResults.find((candidate) => candidate.node.id === selectedNodeId)?.node;
|
||||
return searchNode
|
||||
? {
|
||||
id: searchNode.id,
|
||||
label: searchNode.content || searchNode.id,
|
||||
content: searchNode.content || searchNode.id,
|
||||
nodeType: searchNode.type,
|
||||
color: "#58a6ff",
|
||||
valid_from: null,
|
||||
valid_until: null,
|
||||
properties: searchNode.properties ?? {},
|
||||
neighborCount: 0,
|
||||
visibleNeighborCount: 0,
|
||||
collapsedNeighborCount: 0,
|
||||
isNeighborhoodCollapsed: false,
|
||||
canCollapseNeighborhood: false,
|
||||
}
|
||||
: null;
|
||||
}, [neighborCountMap, searchResults, selectedNodeId, selectedNodeState, snapshot]);
|
||||
|
||||
const focusNode = useCallback((nodeId: string) => {
|
||||
setSelectedNodeId(nodeId);
|
||||
setPathResult(null);
|
||||
|
||||
if (!nodeId) {
|
||||
setSelectedNodeState(null);
|
||||
setPredictions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchResults([]);
|
||||
setIsLayoutRunning(false);
|
||||
}, []);
|
||||
|
||||
const handleSearch = useCallback(async () => {
|
||||
if (!searchQuery.trim()) {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchError("");
|
||||
try {
|
||||
const response = await fetch("/api/graph/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: searchQuery, limit: 8 }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setSearchResults(data.results || []);
|
||||
if (data.results?.length) {
|
||||
focusNode(data.results[0].node.id);
|
||||
}
|
||||
} catch (searchFetchError) {
|
||||
setSearchError(searchFetchError instanceof Error ? searchFetchError.message : "Search failed");
|
||||
}
|
||||
}, [focusNode, searchQuery]);
|
||||
|
||||
const handleRunPredictions = useCallback(async () => {
|
||||
if (!selectedNodeId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/enrich/links", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
node_id: selectedNodeId,
|
||||
top_n: 6,
|
||||
candidate_type: predictionType || undefined,
|
||||
min_score: 0,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Link prediction failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setPredictions(data.predictions || []);
|
||||
} catch (predictionError) {
|
||||
console.error("[GraphWorkspaceShell] prediction failed", predictionError);
|
||||
setPredictions([]);
|
||||
}
|
||||
}, [predictionType, selectedNodeId]);
|
||||
|
||||
const handleTracePath = useCallback(async () => {
|
||||
if (!selectedNodeId || !pathTargetId.trim()) return;
|
||||
|
||||
try {
|
||||
const pathParams = new URLSearchParams({
|
||||
source: selectedNodeId,
|
||||
target: pathTargetId.trim(),
|
||||
algorithm: "dijkstra",
|
||||
});
|
||||
const response = await fetch(
|
||||
`/api/graph/path?${pathParams.toString()}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Path lookup failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data: PathResponse = await response.json();
|
||||
setPathResult(data);
|
||||
if (data.path?.length) {
|
||||
const lastStep = data.path[data.path.length - 1];
|
||||
stageRef.current?.focusNode(lastStep);
|
||||
}
|
||||
} catch (pathError) {
|
||||
console.error("[GraphWorkspaceShell] path trace failed", pathError);
|
||||
setPathResult(null);
|
||||
}
|
||||
}, [pathTargetId, selectedNodeId]);
|
||||
|
||||
const handleDownloadProvenance = useCallback(async (format: "json" | "markdown") => {
|
||||
if (!selectedNodeId) return;
|
||||
|
||||
const suffix = format === "markdown" ? "markdown" : "json";
|
||||
const response = await fetch(`/api/provenance/report?node_id=${encodeURIComponent(selectedNodeId)}&format=${suffix}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Provenance report failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `${selectedNodeId}_provenance.${format === "markdown" ? "md" : "json"}`;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(anchor);
|
||||
}, [selectedNodeId]);
|
||||
|
||||
const searchSummary = useMemo(() => {
|
||||
if (!searchResults.length) return null;
|
||||
return `${searchResults.length} search result${searchResults.length === 1 ? "" : "s"}`;
|
||||
}, [searchResults.length]);
|
||||
|
||||
const focusedSummary = useMemo(() => {
|
||||
if (!visibleSelectedNode) return null;
|
||||
if (viewMode === "focused") {
|
||||
const visibleNeighbors = Math.min(visibleSelectedNode.neighborCount, 16);
|
||||
return `${visibleNeighbors + 1} nodes in focused view`;
|
||||
}
|
||||
return `${visibleSelectedNode.neighborCount} direct neighbors highlighted`;
|
||||
}, [viewMode, visibleSelectedNode]);
|
||||
|
||||
const requestViewMode = useCallback((nextViewMode: GraphViewMode) => {
|
||||
if (nextViewMode === "focused") {
|
||||
if (!selectedNodeId) {
|
||||
return;
|
||||
}
|
||||
setViewMode("focused");
|
||||
setIsLayoutRunning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setViewMode("full");
|
||||
}, [selectedNodeId]);
|
||||
|
||||
const showLoadingOverlay =
|
||||
isLoading
|
||||
|| isFetching
|
||||
|| !isGraphStageReady
|
||||
|| (layoutStatus.source === "runtime" && !layoutStatus.layoutReady && !selectedNodeId && viewMode === "full");
|
||||
|
||||
const layoutStatusLabel = useMemo(() => {
|
||||
if (layoutStatus.source === "provided" && layoutStatus.layoutReady) return "Persisted layout";
|
||||
if (layoutStatus.source === "carried" && layoutStatus.layoutReady) return "Preserved layout";
|
||||
if (layoutStatus.state === "bootstrapping") return "Bootstrapping layout";
|
||||
if (layoutStatus.state === "running") return "Stabilizing layout";
|
||||
if (layoutStatus.state === "failed") return "Layout timeout fallback";
|
||||
return null;
|
||||
}, [layoutStatus]);
|
||||
|
||||
return (
|
||||
<div className="palantir-bg" style={{ position: "relative", width: "100%", height: "100%", overflow: "hidden", display: "flex", flexDirection: "column" }}>
|
||||
<style>{HUD_CSS}</style>
|
||||
<div className="palantir-grid" />
|
||||
<div className="palantir-vignette" />
|
||||
|
||||
<div style={{ flex: 1, position: "relative", zIndex: 3, minHeight: 0 }}>
|
||||
<Suspense fallback={null}>
|
||||
<GraphRuntimeStage
|
||||
ref={stageRef}
|
||||
snapshot={snapshot}
|
||||
selectedNodeId={selectedNodeId}
|
||||
activePath={pathResult?.path ?? []}
|
||||
onNodeSelect={focusNode}
|
||||
onSelectedNodeStateChange={handleSelectedNodeStateChange}
|
||||
isLayoutRunning={isLayoutRunning}
|
||||
onLayoutRunningChange={handleLayoutRunningChange}
|
||||
viewMode={viewMode}
|
||||
temporalTime={debouncedTime}
|
||||
onActiveNodeCountChange={handleActiveNodeCountChange}
|
||||
onProgressChange={handleProgressChange}
|
||||
onLayoutStatusChange={handleLayoutStatusChange}
|
||||
onRuntimeReady={handleRuntimeReady}
|
||||
/>
|
||||
</Suspense>
|
||||
<GraphLoadingOverlay
|
||||
progress={loadingProgress}
|
||||
visible={showLoadingOverlay}
|
||||
showGraphBehind={Boolean(loadingProgress?.showGraphBehind || isGraphStageReady)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<TimelineFallback min={temporalBounds?.min ?? null} max={temporalBounds?.max ?? null} />}>
|
||||
<TimelinePanel
|
||||
onTimeChange={setScrubberTime}
|
||||
minDate={temporalBounds?.min ?? undefined}
|
||||
maxDate={temporalBounds?.max ?? undefined}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<div style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 10 }}>
|
||||
<div className="graph-shell-top">
|
||||
<section className="graph-status-card">
|
||||
<div className="graph-status-label">Graph Studio</div>
|
||||
<div className="graph-status-title">{visibleSelectedNode ? visibleSelectedNode.label : "Knowledge Explorer"}</div>
|
||||
<div className="graph-status-metrics">
|
||||
{showLoadingOverlay && loadingProgress ? <span style={{ ...metricPillStyle, color: "#a9ddff" }}>{getGraphLoadTitle(loadingProgress.phase)}</span> : null}
|
||||
{layoutStatusLabel ? <span style={{ ...metricPillStyle, color: "#a9ddff" }}>{layoutStatusLabel}</span> : null}
|
||||
{snapshot ? <span style={metricPillStyle}>{snapshot.summary.nodeCount.toLocaleString()} nodes · {snapshot.summary.edgeCount.toLocaleString()} edges</span> : null}
|
||||
{activeNodeCount !== null ? <span style={{ ...metricPillStyle, color: "#4fd49c", borderColor: "rgba(79, 212, 156, 0.22)" }}>{activeNodeCount.toLocaleString()} active</span> : null}
|
||||
{searchSummary ? <span style={metricPillStyle}>{searchSummary}</span> : null}
|
||||
{focusedSummary ? <span style={{ ...metricPillStyle, color: "#f2b66d", borderColor: "rgba(242, 182, 109, 0.24)" }}>{focusedSummary}</span> : null}
|
||||
{isError ? <span style={{ ...metricPillStyle, color: "#ff8f85", borderColor: "rgba(255, 123, 114, 0.22)" }}>{(error as Error).message}</span> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="graph-command-card">
|
||||
<div className="graph-command-row">
|
||||
<div className="graph-toggle-cluster">
|
||||
{selectedNodeId ? (
|
||||
<>
|
||||
<button onClick={() => requestViewMode("focused")} style={{ ...actionButtonStyle, background: viewMode === "focused" ? "rgba(31, 111, 235, 0.38)" : actionButtonStyle.background, borderColor: viewMode === "focused" ? "rgba(127, 208, 255, 0.42)" : "rgba(88, 166, 255, 0.2)" }}>Focused View</button>
|
||||
<button onClick={() => requestViewMode("full")} style={{ ...actionButtonStyle, background: viewMode === "full" ? "rgba(31, 111, 235, 0.38)" : actionButtonStyle.background, borderColor: viewMode === "full" ? "rgba(127, 208, 255, 0.42)" : "rgba(88, 166, 255, 0.2)" }}>Full Graph</button>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ color: "#7f95b3", fontSize: 12 }}>Select a node to switch graph views</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="graph-action-cluster">
|
||||
<button onClick={() => setIsLayoutRunning((value) => !value)} style={secondaryActionButtonStyle} disabled={isLoading || isFetching}>
|
||||
{isLayoutRunning ? "Pause Layout" : "Run Layout"}
|
||||
</button>
|
||||
<button onClick={() => { setIsGraphStageReady(false); reload(); }} style={secondaryActionButtonStyle} disabled={isLoading || isFetching}>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="graph-command-row">
|
||||
<div className="graph-search-shell">
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
void handleSearch();
|
||||
}
|
||||
}}
|
||||
placeholder="Search a node, e.g. Metformin"
|
||||
style={{ ...inputStyle, minWidth: 260 }}
|
||||
disabled={showLoadingOverlay && !selectedNodeId}
|
||||
/>
|
||||
<button onClick={() => void handleSearch()} style={actionButtonStyle} disabled={showLoadingOverlay && !selectedNodeId}>Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{searchError ? <div style={{ position: "absolute", top: 144, right: 34, color: "#ff7b72", fontSize: 12, pointerEvents: "auto" }}>{searchError}</div> : null}
|
||||
{searchResults.length ? (
|
||||
<div className="graph-search-results hud-scrollbar">
|
||||
<div className="graph-search-results-label">Search Results</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{searchResults.map((result) => (
|
||||
<button key={result.node.id} className="graph-search-result-card" onClick={() => focusNode(result.node.id)}>
|
||||
<div style={{ color: "#fff", fontWeight: 700 }}>{result.node.content || result.node.id}</div>
|
||||
<div style={{ color: "#8b949e", fontSize: 12 }}>{result.node.type}</div>
|
||||
<div style={{ color: "#58a6ff", fontSize: 12, marginTop: 4 }}>score {result.score.toFixed(3)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="graph-inspector hud-scrollbar" data-open={selectedNodeId ? "true" : "false"}>
|
||||
<NodePanel
|
||||
node={visibleSelectedNode}
|
||||
predictions={predictions}
|
||||
predictionType={predictionType}
|
||||
onPredictionTypeChange={setPredictionType}
|
||||
onRunPredictions={() => void handleRunPredictions()}
|
||||
pathTargetId={pathTargetId}
|
||||
onPathTargetChange={setPathTargetId}
|
||||
onTracePath={() => void handleTracePath()}
|
||||
pathResult={pathResult}
|
||||
onDownloadProvenance={(format) => void handleDownloadProvenance(format)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const metricPillStyle: CSSProperties = {
|
||||
background: "rgba(88, 166, 255, 0.08)",
|
||||
color: "#8ed3ff",
|
||||
padding: "6px 11px",
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
border: "1px solid rgba(88, 166, 255, 0.14)",
|
||||
};
|
||||
|
||||
const sectionStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
padding: 14,
|
||||
background: "linear-gradient(180deg, rgba(255,255,255,0.025), rgba(255,255,255,0.01))",
|
||||
border: "1px solid rgba(255, 255, 255, 0.06)",
|
||||
borderRadius: 16,
|
||||
};
|
||||
|
||||
const sectionTitleStyle: CSSProperties = {
|
||||
color: "#8fa8c6",
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
};
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
background: "rgba(0, 0, 0, 0.24)",
|
||||
border: "1px solid rgba(88, 166, 255, 0.14)",
|
||||
color: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: "10px 12px",
|
||||
fontSize: 13,
|
||||
};
|
||||
|
||||
const actionButtonStyle: CSSProperties = {
|
||||
background: "linear-gradient(180deg, rgba(53, 130, 245, 0.28), rgba(25, 88, 185, 0.18))",
|
||||
color: "#fff",
|
||||
border: "1px solid rgba(88, 166, 255, 0.2)",
|
||||
borderRadius: 12,
|
||||
padding: "10px 13px",
|
||||
cursor: "pointer",
|
||||
fontWeight: 700,
|
||||
fontSize: 12,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.05)",
|
||||
};
|
||||
|
||||
const secondaryActionButtonStyle: CSSProperties = {
|
||||
...actionButtonStyle,
|
||||
background: "rgba(255, 255, 255, 0.035)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.06)",
|
||||
color: "#d6e5f8",
|
||||
fontWeight: 500,
|
||||
};
|
||||
|
||||
const predictionCardStyle: CSSProperties = {
|
||||
textAlign: "left",
|
||||
padding: 12,
|
||||
background: "rgba(88, 166, 255, 0.06)",
|
||||
border: "1px solid rgba(88, 166, 255, 0.1)",
|
||||
borderRadius: 14,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const pathStepStyle: CSSProperties = {
|
||||
color: "#e6edf3",
|
||||
fontSize: 13,
|
||||
padding: "8px 10px",
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
borderRadius: 8,
|
||||
};
|
||||
|
||||
const propertyCardStyle: CSSProperties = {
|
||||
background: "rgba(0, 0, 0, 0.18)",
|
||||
padding: "10px 12px",
|
||||
borderRadius: 12,
|
||||
border: "1px solid rgba(255, 255, 255, 0.05)",
|
||||
};
|
||||
|
||||
const emptyTextStyle: CSSProperties = {
|
||||
color: "#8b949e",
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
};
|
||||
|
||||
const subtleChipStyle: CSSProperties = {
|
||||
background: "rgba(255, 255, 255, 0.035)",
|
||||
color: "#9fb6d2",
|
||||
padding: "5px 9px",
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
border: "1px solid rgba(255, 255, 255, 0.06)",
|
||||
};
|
||||
|
||||
const collapseStyle: CSSProperties = {
|
||||
border: "1px solid rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: 14,
|
||||
background: "rgba(0, 0, 0, 0.14)",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const summaryStyle: CSSProperties = {
|
||||
cursor: "pointer",
|
||||
listStyle: "none",
|
||||
padding: "12px 14px",
|
||||
color: "#c6d4e3",
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.04em",
|
||||
textTransform: "uppercase",
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
|
||||
import { GraphCanvas, type GraphCanvasHandle } from "./GraphCanvas";
|
||||
import type { GraphSceneAdapter, GraphSceneHandle, GraphSceneProps, GraphSceneRuntime } from "./scene";
|
||||
|
||||
export const SigmaSceneAdapter = forwardRef<GraphSceneHandle, GraphSceneProps>(
|
||||
function SigmaSceneAdapter(
|
||||
{
|
||||
onNodeSelect,
|
||||
onEdgeSelect,
|
||||
onInteractionStateChange,
|
||||
onCameraStateChange,
|
||||
onDiagnosticsChange,
|
||||
onAnalyticsChange,
|
||||
onRuntimeChange,
|
||||
onLayoutRunningChange,
|
||||
...sceneProps
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const canvasRef = useRef<GraphCanvasHandle>(null);
|
||||
const runtimeRef = useRef<GraphSceneRuntime | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
fitView: () => canvasRef.current?.fitView(),
|
||||
focusNode: (nodeId: string) => canvasRef.current?.focusNode(nodeId),
|
||||
zoomIn: () => canvasRef.current?.zoomIn(),
|
||||
zoomOut: () => canvasRef.current?.zoomOut(),
|
||||
getRuntime: () => runtimeRef.current,
|
||||
setLayoutRunning: onLayoutRunningChange
|
||||
? (running: boolean) => {
|
||||
onLayoutRunningChange(running);
|
||||
}
|
||||
: undefined,
|
||||
}), [onLayoutRunningChange]);
|
||||
|
||||
return (
|
||||
<GraphCanvas
|
||||
ref={canvasRef}
|
||||
onNodeClick={onNodeSelect ?? (() => {})}
|
||||
onEdgeClick={onEdgeSelect}
|
||||
onInteractionStateChange={onInteractionStateChange}
|
||||
onCameraStateChange={onCameraStateChange}
|
||||
onDiagnosticsChange={onDiagnosticsChange}
|
||||
onAnalyticsChange={onAnalyticsChange}
|
||||
onSceneRuntimeChange={(runtime) => {
|
||||
runtimeRef.current = runtime;
|
||||
onRuntimeChange?.(runtime);
|
||||
}}
|
||||
onLayoutRunningChange={onLayoutRunningChange}
|
||||
{...sceneProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
) as GraphSceneAdapter;
|
||||
@@ -0,0 +1,199 @@
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
||||
import { DataSet } from "vis-data";
|
||||
import { Timeline } from "vis-timeline";
|
||||
import type { TimelineOptions } from "vis-timeline";
|
||||
import "vis-timeline/styles/vis-timeline-graph2d.css";
|
||||
import { GRAPH_THEME } from "./graphTheme";
|
||||
|
||||
export interface TimelinePanelProps {
|
||||
onTimeChange: (time: Date) => void;
|
||||
minDate?: string;
|
||||
maxDate?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_MIN_DATE = new Date("1970-01-01T00:00:00Z");
|
||||
const DEFAULT_MAX_DATE = new Date("2030-01-01T00:00:00Z");
|
||||
const PLAYHEAD_ID = "playhead";
|
||||
const PLAY_INTERVAL_MS = 500;
|
||||
const PLAY_STEP_MONTHS = 6;
|
||||
|
||||
const VIS_OVERRIDE_CSS = `
|
||||
.sem-timeline-wrap .vis-timeline { border: none !important; background: transparent !important; overflow: visible !important; }
|
||||
.sem-timeline-wrap .vis-panel.vis-background, .sem-timeline-wrap .vis-panel.vis-center { background: transparent !important; }
|
||||
.sem-timeline-wrap .vis-panel { border-color: ${GRAPH_THEME.ui.timeline.border} !important; }
|
||||
.sem-timeline-wrap .vis-time-axis .vis-text {
|
||||
color: ${GRAPH_THEME.ui.timeline.text} !important;
|
||||
font-size: 11px !important;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace !important;
|
||||
padding-top: 3px !important;
|
||||
}
|
||||
.sem-timeline-wrap .vis-time-axis .vis-text.vis-major {
|
||||
color: ${GRAPH_THEME.ui.timeline.textStrong} !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.sem-timeline-wrap .vis-time-axis .vis-grid.vis-minor { border-color: ${GRAPH_THEME.ui.timeline.gridMinor} !important; }
|
||||
.sem-timeline-wrap .vis-time-axis .vis-grid.vis-major { border-color: ${GRAPH_THEME.ui.timeline.gridMajor} !important; }
|
||||
.sem-timeline-wrap .vis-custom-time.${PLAYHEAD_ID} {
|
||||
background: ${GRAPH_THEME.ui.timeline.playheadSoft} !important;
|
||||
width: 2px !important;
|
||||
cursor: ew-resize !important;
|
||||
z-index: 5 !important;
|
||||
}
|
||||
.sem-timeline-wrap .vis-custom-time.${PLAYHEAD_ID} > .vis-custom-time-marker {
|
||||
background: ${GRAPH_THEME.ui.timeline.playhead} !important;
|
||||
color: ${GRAPH_THEME.ui.text.inverse} !important;
|
||||
font-size: 10px !important;
|
||||
font-weight: 700 !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 1px 5px !important;
|
||||
white-space: nowrap !important;
|
||||
box-shadow: 0 0 8px rgba(98, 226, 205, 0.45) !important;
|
||||
}
|
||||
.sem-timeline-wrap .vis-current-time { display: none !important; }
|
||||
.sem-timeline-wrap .vis-panel.vis-left { display: none !important; }
|
||||
`;
|
||||
|
||||
function safeDate(value: string | undefined, fallback: Date): Date {
|
||||
if (!value) return fallback;
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? fallback : parsed;
|
||||
}
|
||||
|
||||
function formatPlayheadLabel(value: Date): string {
|
||||
return `${value.getFullYear()}/${String(value.getMonth() + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function TimelinePanel({ onTimeChange, minDate, maxDate }: TimelinePanelProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const timelineRef = useRef<Timeline | null>(null);
|
||||
const playheadRef = useRef<Date>(DEFAULT_MIN_DATE);
|
||||
const playIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [displayDate, setDisplayDate] = useState(formatPlayheadLabel(DEFAULT_MIN_DATE));
|
||||
|
||||
const minBound = useMemo(() => safeDate(minDate, DEFAULT_MIN_DATE), [minDate]);
|
||||
const maxBound = useMemo(() => safeDate(maxDate, DEFAULT_MAX_DATE), [maxDate]);
|
||||
const defaultTime = useMemo(() => new Date(Math.round((minBound.getTime() + maxBound.getTime()) / 2)), [maxBound, minBound]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const timeline = timelineRef.current;
|
||||
if (!timeline) {
|
||||
const items = new DataSet([]);
|
||||
const options: TimelineOptions = {
|
||||
height: "100%",
|
||||
min: minBound,
|
||||
max: maxBound,
|
||||
start: minBound,
|
||||
end: maxBound,
|
||||
showCurrentTime: false,
|
||||
zoomable: true,
|
||||
moveable: true,
|
||||
zoomMin: 1000 * 60 * 60 * 24 * 365,
|
||||
zoomMax: 1000 * 60 * 60 * 24 * 365 * 80,
|
||||
showMajorLabels: true,
|
||||
showMinorLabels: true,
|
||||
timeAxis: { scale: "year", step: 5 },
|
||||
format: { minorLabels: { year: "YYYY" }, majorLabels: { year: "YYYY" } },
|
||||
orientation: { axis: "bottom" },
|
||||
margin: { item: 0, axis: 0 },
|
||||
selectable: false,
|
||||
stack: false,
|
||||
} as TimelineOptions;
|
||||
|
||||
const nextTimeline = new Timeline(containerRef.current, items, options);
|
||||
timelineRef.current = nextTimeline;
|
||||
playheadRef.current = defaultTime;
|
||||
nextTimeline.addCustomTime(defaultTime, PLAYHEAD_ID);
|
||||
nextTimeline.on("timechange", (props: { id: string; time: Date }) => {
|
||||
if (props.id !== PLAYHEAD_ID) return;
|
||||
playheadRef.current = props.time;
|
||||
nextTimeline.setCustomTime(props.time, PLAYHEAD_ID);
|
||||
onTimeChange(props.time);
|
||||
setDisplayDate(formatPlayheadLabel(props.time));
|
||||
});
|
||||
onTimeChange(defaultTime);
|
||||
setDisplayDate(formatPlayheadLabel(defaultTime));
|
||||
return () => {
|
||||
nextTimeline.destroy();
|
||||
timelineRef.current = null;
|
||||
};
|
||||
}
|
||||
|
||||
timeline.setOptions({ min: minBound, max: maxBound, start: minBound, end: maxBound });
|
||||
playheadRef.current = defaultTime;
|
||||
timeline.setCustomTime(defaultTime, PLAYHEAD_ID);
|
||||
onTimeChange(defaultTime);
|
||||
setDisplayDate(formatPlayheadLabel(defaultTime));
|
||||
}, [defaultTime, maxBound, minBound, onTimeChange]);
|
||||
|
||||
const startPlay = useCallback(() => {
|
||||
if (playIntervalRef.current) return;
|
||||
playIntervalRef.current = setInterval(() => {
|
||||
const timeline = timelineRef.current;
|
||||
if (!timeline) return;
|
||||
const next = new Date(playheadRef.current);
|
||||
next.setMonth(next.getMonth() + PLAY_STEP_MONTHS);
|
||||
if (next >= maxBound) {
|
||||
next.setTime(minBound.getTime());
|
||||
}
|
||||
playheadRef.current = next;
|
||||
timeline.setCustomTime(next, PLAYHEAD_ID);
|
||||
onTimeChange(next);
|
||||
setDisplayDate(formatPlayheadLabel(next));
|
||||
}, PLAY_INTERVAL_MS);
|
||||
}, [maxBound, minBound, onTimeChange]);
|
||||
|
||||
const stopPlay = useCallback(() => {
|
||||
if (playIntervalRef.current) {
|
||||
clearInterval(playIntervalRef.current);
|
||||
playIntervalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
setIsPlaying((previous) => {
|
||||
if (previous) {
|
||||
stopPlay();
|
||||
return false;
|
||||
}
|
||||
startPlay();
|
||||
return true;
|
||||
});
|
||||
}, [startPlay, stopPlay]);
|
||||
|
||||
useEffect(() => () => stopPlay(), [stopPlay]);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width: "100%", height: "90px", borderTop: `1px solid ${GRAPH_THEME.ui.timeline.border}`, background: GRAPH_THEME.ui.timeline.background, backdropFilter: "blur(16px)", WebkitBackdropFilter: "blur(16px)", display: "flex", alignItems: "stretch", flexShrink: 0 }}>
|
||||
<style>{VIS_OVERRIDE_CSS}</style>
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 4, padding: "0 16px", borderRight: `1px solid ${GRAPH_THEME.ui.timeline.border}`, minWidth: 80, flexShrink: 0 }}>
|
||||
<button
|
||||
id="temporal-play-btn"
|
||||
onClick={togglePlay}
|
||||
title={isPlaying ? "Pause Evolution" : "Play Evolution"}
|
||||
style={{ width: 34, height: 34, borderRadius: "50%", border: `1.5px solid ${isPlaying ? GRAPH_THEME.ui.control.activeBorder : GRAPH_THEME.ui.control.defaultBorder}`, background: isPlaying ? GRAPH_THEME.ui.timeline.playheadSoft : GRAPH_THEME.ui.control.defaultBg, color: GRAPH_THEME.ui.timeline.playhead, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.2s", boxShadow: isPlaying ? "0 0 10px rgba(98, 226, 205, 0.32)" : "none" }}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" /></svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5,3 19,12 5,21" /></svg>
|
||||
)}
|
||||
</button>
|
||||
<span style={{ fontSize: 10, color: isPlaying ? GRAPH_THEME.ui.timeline.playhead : GRAPH_THEME.ui.timeline.text, fontFamily: "monospace", letterSpacing: "0.04em", transition: "color 0.2s" }}>
|
||||
{displayDate}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ position: "absolute", top: 5, left: 100, fontSize: 10, fontWeight: 600, letterSpacing: "0.1em", color: GRAPH_THEME.ui.text.subtle, textTransform: "uppercase", pointerEvents: "none", zIndex: 2 }}>
|
||||
Temporal Scrubber · {minBound.getFullYear()}-{maxBound.getFullYear()}
|
||||
</div>
|
||||
|
||||
<div className="sem-timeline-wrap" style={{ flex: 1, overflow: "hidden", position: "relative" }}>
|
||||
<div ref={containerRef} style={{ width: "100%", height: "100%", position: "relative" }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { GraphBehavior } from "./types";
|
||||
|
||||
export const clickSelectionBehavior: GraphBehavior = {
|
||||
id: "click-selection",
|
||||
attach: () => {},
|
||||
detach: () => {},
|
||||
onNodeClick: (context, nodeId) => {
|
||||
context.setHoveredNodeId(nodeId);
|
||||
context.onEdgeSelectionChange("");
|
||||
if (context.getInteractionState().selectedNodeId === nodeId) {
|
||||
context.onNodeSelectionChange("");
|
||||
} else {
|
||||
context.onNodeSelectionChange(nodeId);
|
||||
}
|
||||
},
|
||||
onEdgeClick: (context, edgeId) => {
|
||||
context.setHoveredNodeId(null);
|
||||
if (context.getInteractionState().selectedEdgeId === edgeId) {
|
||||
context.onEdgeSelectionChange("");
|
||||
} else {
|
||||
context.onEdgeSelectionChange(edgeId);
|
||||
}
|
||||
},
|
||||
onStageClick: (context) => {
|
||||
context.setHoveredNodeId(null);
|
||||
context.onEdgeSelectionChange("");
|
||||
context.onNodeSelectionChange("");
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user