mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Compare commits
450
Commits
provenance
..
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d04005edf | ||
|
|
282418c953 | ||
|
|
c77690129d | ||
|
|
655a24b77e | ||
|
|
ba050acc7d | ||
|
|
8faa87dace | ||
|
|
4747d403bc | ||
|
|
8348df63be | ||
|
|
68b8b370d6 | ||
|
|
f8ec5ac010 | ||
|
|
40fe1d587a | ||
|
|
29a608f60e | ||
|
|
907f0e8f45 | ||
|
|
0c213f1483 | ||
|
|
a51542ce40 | ||
|
|
08150fb2f7 | ||
|
|
ce01067009 | ||
|
|
a896c36389 | ||
|
|
5f55e9b363 | ||
|
|
25999076df | ||
|
|
2dbc50a2fe | ||
|
|
790ff71c0a | ||
|
|
af57e5269d | ||
|
|
88309da972 | ||
|
|
b0947df934 | ||
|
|
a036c4405b | ||
|
|
0365712a8b | ||
|
|
f7170cd6df | ||
|
|
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 | ||
|
|
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 | ||
|
|
d5e2637fbd | ||
|
|
f5896574c6 | ||
|
|
0fa68be018 | ||
|
|
5e1bdf08f9 | ||
|
|
8eda00304d | ||
|
|
8aa2ee3dc8 | ||
|
|
3f211dfb23 | ||
|
|
23da9c2fb8 | ||
|
|
53a14fa897 | ||
|
|
d69d4f5b67 | ||
|
|
43eb4535d8 | ||
|
|
a60791d815 | ||
|
|
c31df5c4d7 | ||
|
|
c6ace4c6c1 | ||
|
|
a785247b98 | ||
|
|
8bd4df74e1 | ||
|
|
f9f19f343e | ||
|
|
89d60301ce | ||
|
|
0a63128cbd | ||
|
|
ab2df6d4ee | ||
|
|
41530da25f | ||
|
|
17b0a24257 | ||
|
|
a98f21e5d3 | ||
|
|
bcb9a65a20 | ||
|
|
3872ea75e1 | ||
|
|
20b5f7c0ab | ||
|
|
2cee7d84fa | ||
|
|
1a5e34dee8 | ||
|
|
ad7d9266c1 | ||
|
|
99aae252cf | ||
|
|
c2a627a998 | ||
|
|
ff957be6a8 | ||
|
|
471542087d | ||
|
|
59ae0bdf44 | ||
|
|
49c60387c5 | ||
|
|
e3ec5b151a | ||
|
|
ca3cd1ded5 | ||
|
|
e37a54999f | ||
|
|
33c90d8277 | ||
|
|
f704d6ce91 | ||
|
|
dcb4f77efc | ||
|
|
28dc1ed4e9 | ||
|
|
94448e1e5d | ||
|
|
692247c559 | ||
|
|
2801cd7438 | ||
|
|
e88781472b | ||
|
|
fd21ec8c77 | ||
|
|
fcf0c684bd | ||
|
|
3589f3b807 | ||
|
|
8e83d11479 | ||
|
|
79d554767d | ||
|
|
66e971d0f8 | ||
|
|
14f5e05336 | ||
|
|
adddf82242 | ||
|
|
f2a042c796 | ||
|
|
20755e69e2 | ||
|
|
b42bfaef09 | ||
|
|
1e4798ca0d | ||
|
|
d2f8992ca9 | ||
|
|
e51dd9d655 | ||
|
|
4e31296c1e | ||
|
|
780f8adfbe | ||
|
|
8386d79543 | ||
|
|
5d712d5a62 | ||
|
|
47c0058dce | ||
|
|
b90ffcca9a | ||
|
|
4cd3ef9aa8 | ||
|
|
2df5edf30a | ||
|
|
0bc41fb39a | ||
|
|
381224dcdc | ||
|
|
db64dce596 | ||
|
|
b5aec8b832 | ||
|
|
b36e09d282 | ||
|
|
560661e66a | ||
|
|
ac51b74928 | ||
|
|
7a24273f41 | ||
|
|
7a25a7791e | ||
|
|
17fc42ccaa | ||
|
|
62bf3bada9 | ||
|
|
e933c5ad69 | ||
|
|
07d9193719 | ||
|
|
c41cc28fff | ||
|
|
7ad48df600 | ||
|
|
d766d0c287 | ||
|
|
bb14ebcdda | ||
|
|
a77299b59b | ||
|
|
bbbc2fb126 | ||
|
|
385a617f89 | ||
|
|
bb95c00a88 | ||
|
|
7c7a903a3b | ||
|
|
64ce8497f4 | ||
|
|
7bb6a2291e | ||
|
|
cc70238c4c | ||
|
|
efabbdb538 | ||
|
|
1ad09781a2 | ||
|
|
3a59fb8da6 | ||
|
|
852bf0596d | ||
|
|
0254843fa3 | ||
|
|
b473285dcb | ||
|
|
95322df8e0 | ||
|
|
52ab28659b | ||
|
|
99b3c1524a | ||
|
|
52da99652f | ||
|
|
163318da1f | ||
|
|
3f60f2c8c3 | ||
|
|
5cf41c9f92 | ||
|
|
27bf2351b8 | ||
|
|
4bf1d41f99 | ||
|
|
b219af9fc5 | ||
|
|
6fc69aef2e | ||
|
|
6daf4c9c67 | ||
|
|
b224326ae7 | ||
|
|
e9d8181e93 | ||
|
|
f02cda2638 | ||
|
|
db1e3a5050 | ||
|
|
5e23007658 | ||
|
|
c45b4b5d4c | ||
|
|
5f947c8eea | ||
|
|
d108c6f4fd | ||
|
|
f73de529bf | ||
|
|
893e93e575 | ||
|
|
7c75567833 | ||
|
|
34adf94f01 | ||
|
|
3381a1f5ff | ||
|
|
b78f03872a | ||
|
|
96d06c64db | ||
|
|
68e5865dd0 | ||
|
|
402d5ed2d6 | ||
|
|
f6992066d9 | ||
|
|
8ba020a3ab | ||
|
|
ec7528e96c | ||
|
|
a108a54b58 | ||
|
|
854f7cbb8c | ||
|
|
affe3aa8bd | ||
|
|
ae8cbcde68 | ||
|
|
7c6a921a51 | ||
|
|
b4cfb6df15 | ||
|
|
e182f10d22 | ||
|
|
1d055095ee | ||
|
|
17428fdb08 | ||
|
|
1d7bd6f5d8 | ||
|
|
3f12e78ca0 | ||
|
|
1ff05eef42 | ||
|
|
e5e012cb5e | ||
|
|
48114a1d86 | ||
|
|
21269ea501 | ||
|
|
ade63932b0 | ||
|
|
b9326cfbfd | ||
|
|
d5b06b878e | ||
|
|
579d8909fb | ||
|
|
9b05622f8c | ||
|
|
5e13d925be | ||
|
|
ad06957f93 | ||
|
|
33e6a94407 | ||
|
|
f45b7a26ba | ||
|
|
d0e2cacec3 | ||
|
|
89d2bca802 | ||
|
|
d3b579208c | ||
|
|
d7cc4afc91 | ||
|
|
e47327ebb5 | ||
|
|
d0bf15465d | ||
|
|
d6f4317f0e | ||
|
|
826f3d964d | ||
|
|
2dd756d0b8 | ||
|
|
92be781472 | ||
|
|
2d155b744e | ||
|
|
85e302bbc0 | ||
|
|
0a66e1c6ea | ||
|
|
06d5fad6b9 | ||
|
|
344a3a6fda | ||
|
|
e9dfcff873 | ||
|
|
a4ab3fd9e3 | ||
|
|
687804d0b4 | ||
|
|
804de2c13c | ||
|
|
4ab8b4d72b | ||
|
|
6133451d23 | ||
|
|
8a295f97ce | ||
|
|
d4842daf07 | ||
|
|
d5c376b4dd |
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"projectName": "Semantica",
|
||||
"projectOwner": "Hawksight-AI",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"CONTRIBUTORS.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": true,
|
||||
"commitConvention": "conventional",
|
||||
"contributors": [],
|
||||
"contributorsPerLine": 7,
|
||||
"badgeTemplate": "[](#contributors)",
|
||||
"skipCi": true
|
||||
}
|
||||
|
||||
+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/semantica) 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):
|
||||
|
||||
+117
-15
@@ -1,28 +1,130 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
# Python dependencies (pip/pyproject.toml)
|
||||
# Core Python dependencies
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly" # Weekly for security
|
||||
day: "monday"
|
||||
time: "03:30" # 3:30 AM UTC (9:00 AM IST)
|
||||
open-pull-requests-limit: 10 # Higher limit for security updates
|
||||
reviewers:
|
||||
- "KaifAhmad1"
|
||||
assignees:
|
||||
- "KaifAhmad1"
|
||||
commit-message:
|
||||
prefix: "security"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "python"
|
||||
- "security"
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
- dependency-type: "development"
|
||||
ignore:
|
||||
# Only ignore major version updates for stability-critical packages
|
||||
- dependency-name: "torch"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "transformers"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# Group new feature dependencies
|
||||
groups:
|
||||
security-critical:
|
||||
patterns:
|
||||
- "cryptography"
|
||||
- "requests"
|
||||
- "urllib3"
|
||||
- "certifi"
|
||||
- "pyopenssl"
|
||||
dependency-type: "production"
|
||||
snowflake-features:
|
||||
patterns:
|
||||
- "snowflake-connector-python"
|
||||
- "cryptography"
|
||||
arrow-features:
|
||||
patterns:
|
||||
- "pyarrow"
|
||||
benchmark-tools:
|
||||
patterns:
|
||||
- "pytest-benchmark"
|
||||
- "pytest-cov"
|
||||
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "09:00"
|
||||
open-pull-requests-limit: 0
|
||||
ignore:
|
||||
# Ignore all updates (no PRs will be created)
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"]
|
||||
open-pull-requests-limit: 3
|
||||
reviewers:
|
||||
- "KaifAhmad1"
|
||||
assignees:
|
||||
- "KaifAhmad1"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
- "ci"
|
||||
|
||||
# GitHub Actions dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
# Optional dependencies (separate schedule for stability)
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
day: "monday"
|
||||
interval: "weekly"
|
||||
day: "friday"
|
||||
time: "09:00"
|
||||
open-pull-requests-limit: 0
|
||||
ignore:
|
||||
# Ignore all updates (no PRs will be created)
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"]
|
||||
target-branch: "main"
|
||||
open-pull-requests-limit: 3
|
||||
reviewers:
|
||||
- "KaifAhmad1"
|
||||
assignees:
|
||||
- "KaifAhmad1"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "python"
|
||||
- "optional"
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
|
||||
# Docker dependencies (if you use Docker)
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "wednesday"
|
||||
time: "09:00"
|
||||
open-pull-requests-limit: 2
|
||||
reviewers:
|
||||
- "KaifAhmad1"
|
||||
assignees:
|
||||
- "KaifAhmad1"
|
||||
commit-message:
|
||||
prefix: "docker"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "docker"
|
||||
|
||||
# Documentation dependencies
|
||||
- package-ecosystem: "pip"
|
||||
directory: "docs"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 2
|
||||
reviewers:
|
||||
- "KaifAhmad1"
|
||||
commit-message:
|
||||
prefix: "docs"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "documentation"
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Semantica Performance Suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
performance-test:
|
||||
name: Benchmark Runner (Ubuntu/Python 3.12)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
|
||||
BENCHMARK_REAL_LIBS: "1"
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .
|
||||
pip install -r benchmarks/requirements.txt
|
||||
python -m spacy download en_core_web_sm
|
||||
pip install rdflib neo4j faiss-cpu torch pyarrow pdfplumber python-pptx openpyxl lxml python-docx beautifulsoup4 chardet langdetect
|
||||
|
||||
- name: Execute Benchmarks (Real Mode)
|
||||
env:
|
||||
BENCHMARK_REAL_LIBS: "1"
|
||||
run: |
|
||||
python benchmarks/benchmarks_runner.py
|
||||
# Optional: Compare to baseline (requires previous run artifact)
|
||||
# pytest-benchmark --storage file://benchmarks/results --benchmark-compare
|
||||
|
||||
- name: Upload Benchmark Results
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: benchmark-report-${{ github.run_id }}
|
||||
path: benchmarks/results
|
||||
retention-days: 30
|
||||
@@ -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,86 @@
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
dismiss-fixed-alerts:
|
||||
name: Dismiss Fixed Security Alerts
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
steps:
|
||||
- name: Dismiss resolved CodeQL alerts via API
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
FIXED_PATTERNS=(
|
||||
"py/clear-text-logging-sensitive-data"
|
||||
"py/incomplete-url-substring-sanitization"
|
||||
"actions/missing-workflow-permissions"
|
||||
)
|
||||
|
||||
# Fetch all open code scanning alerts
|
||||
ALERTS=$(gh api repos/$REPO/code-scanning/alerts \
|
||||
--jq '.[] | {number: .number, rule: .rule.id, state: .state}' \
|
||||
-X GET -f state=open -f per_page=100)
|
||||
|
||||
for PATTERN in "${FIXED_PATTERNS[@]}"; do
|
||||
ALERT_NUMS=$(echo "$ALERTS" | jq -r \
|
||||
"select(.rule == \"$PATTERN\") | .number")
|
||||
for NUM in $ALERT_NUMS; do
|
||||
echo "Dismissing alert #$NUM ($PATTERN) — fixed in security-enhancement PR"
|
||||
gh api repos/$REPO/code-scanning/alerts/$NUM \
|
||||
-X PATCH \
|
||||
-f state=dismissed \
|
||||
-f dismissed_reason="won't fix" \
|
||||
-f dismissed_comment="Fixed in PR security-enhancement: code changes remove the vulnerability. Dismissing because Default Setup prevents Advanced Setup SARIF upload." \
|
||||
&& echo " ✓ Alert #$NUM dismissed" \
|
||||
|| echo " ⚠ Could not dismiss alert #$NUM (may already be closed)"
|
||||
done
|
||||
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,7 +59,7 @@ 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
|
||||
@@ -76,4 +77,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * 1,4' # Mon/Thu 7 AM IST
|
||||
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:
|
||||
security-scan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install safety bandit semgrep jq
|
||||
|
||||
- name: Run Safety Check (Package Vulnerabilities)
|
||||
run: |
|
||||
safety check --json --output safety-report.json || true
|
||||
echo "Checking for package vulnerabilities..."
|
||||
|
||||
# Count vulnerabilities safely
|
||||
VULNS=$(safety check --json --output /dev/stdout 2>/dev/null | jq '.vulnerabilities | length' 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$VULNS" -gt 0 ]; then
|
||||
echo "❌ Security vulnerabilities found: $VULNS"
|
||||
echo "CI will fail to prevent merging of vulnerable dependencies"
|
||||
echo ""
|
||||
echo "Vulnerability details:"
|
||||
safety check || true
|
||||
exit 1
|
||||
else
|
||||
echo "✅ No security vulnerabilities found"
|
||||
fi
|
||||
|
||||
- name: Run Bandit (Code Security Linter)
|
||||
run: |
|
||||
bandit -r semantica/ -f json -o bandit-report.json || true
|
||||
echo "Checking for HIGH severity security issues..."
|
||||
|
||||
# Count HIGH severity issues
|
||||
HIGH_ISSUES=$(bandit -r semantica/ -f json -ll 2>/dev/null | jq -r '.results[]? | select(.issue_severity == "HIGH") | .test_name' 2>/dev/null | wc -l || echo "0")
|
||||
|
||||
if [ "$HIGH_ISSUES" -gt 0 ]; then
|
||||
echo "❌ HIGH severity security issues found: $HIGH_ISSUES"
|
||||
echo "CI will fail to prevent merging of high-risk code"
|
||||
echo ""
|
||||
echo "High severity issues:"
|
||||
bandit -r semantica/ -ll | grep "Severity: High" -A 5 -B 1 || true
|
||||
exit 1
|
||||
else
|
||||
echo "✅ No HIGH severity security issues found"
|
||||
fi
|
||||
|
||||
- name: Run Semgrep (Static Analysis)
|
||||
run: |
|
||||
echo "Running Semgrep static analysis..."
|
||||
semgrep --config=auto --json --output=semgrep-report.json semantica/ || true
|
||||
|
||||
# Run security-focused rules
|
||||
echo "Checking for security patterns..."
|
||||
SECURITY_ISSUES=$(semgrep --config=p/security --json semantica/ 2>/dev/null | jq '.results | length' 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$SECURITY_ISSUES" -gt 0 ]; then
|
||||
echo "⚠️ Security patterns found: $SECURITY_ISSUES"
|
||||
echo "Review these findings for potential improvements"
|
||||
semgrep --config=p/security semantica/ || true
|
||||
else
|
||||
echo "✅ No security patterns found"
|
||||
fi
|
||||
|
||||
- name: Upload Security Reports
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: security-reports
|
||||
path: |
|
||||
safety-report.json
|
||||
bandit-report.json
|
||||
semgrep-report.json
|
||||
|
||||
- name: Comment PR with Security Results
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Read safety report
|
||||
let safetyResults = '';
|
||||
try {
|
||||
const safetyData = JSON.parse(fs.readFileSync('safety-report.json', 'utf8'));
|
||||
if (safetyData.vulnerabilities && safetyData.vulnerabilities.length > 0) {
|
||||
safetyResults = `## Safety Vulnerabilities Found\\n`;
|
||||
safetyData.vulnerabilities.forEach(vuln => {
|
||||
safetyResults += `- **${vuln.package}**: ${vuln.advisory}\\n`;
|
||||
});
|
||||
} else {
|
||||
safetyResults = '## No Safety Vulnerabilities Found\\n';
|
||||
}
|
||||
} catch (e) {
|
||||
safetyResults = '## Safety scan completed\\n';
|
||||
}
|
||||
|
||||
// Read bandit report
|
||||
let banditResults = '';
|
||||
try {
|
||||
const banditData = JSON.parse(fs.readFileSync('bandit-report.json', 'utf8'));
|
||||
if (banditData.results && banditData.results.length > 0) {
|
||||
const highIssues = banditData.results.filter(issue => issue.issue_severity === 'HIGH');
|
||||
if (highIssues.length > 0) {
|
||||
banditResults = `## High Severity Security Issues Found\\n`;
|
||||
highIssues.forEach(issue => {
|
||||
banditResults += `- **${issue.test_name}**: ${issue.filename}:${issue.line_number}\\n`;
|
||||
});
|
||||
} else {
|
||||
banditResults = '## No High Severity Security Issues Found\\n';
|
||||
}
|
||||
} else {
|
||||
banditResults = '## No Bandit Issues Found\\n';
|
||||
}
|
||||
} catch (e) {
|
||||
banditResults = '## Bandit scan completed\\n';
|
||||
}
|
||||
|
||||
// Read semgrep report
|
||||
let semgrepResults = '';
|
||||
try {
|
||||
const semgrepData = JSON.parse(fs.readFileSync('semgrep-report.json', 'utf8'));
|
||||
if (semgrepData.results && semgrepData.results.length > 0) {
|
||||
semgrepResults = `## Security Patterns Found\\n`;
|
||||
semgrepData.results.slice(0, 10).forEach(issue => {
|
||||
semgrepResults += `- **${issue.rule_id}**: ${issue.path}\\n`;
|
||||
});
|
||||
if (semgrepData.results.length > 10) {
|
||||
semgrepResults += `- ... and ${semgrepData.results.length - 10} more\\n`;
|
||||
}
|
||||
} else {
|
||||
semgrepResults = '## No Security Patterns Found\\n';
|
||||
}
|
||||
} catch (e) {
|
||||
semgrepResults = '## Semgrep scan completed\\n';
|
||||
}
|
||||
|
||||
// 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 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 {
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
console.log('✅ Security comment posted successfully');
|
||||
} catch (error) {
|
||||
console.log('⚠️ Could not post security comment:', error.message);
|
||||
console.log('📋 Security scan results saved to artifacts');
|
||||
}
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+677
-1
@@ -7,6 +7,683 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.0] - 2026-04-08
|
||||
|
||||
- **Named Graph Support: Review Follow-up Fixes** (PR #432 by @Sameer6305, follow-up patch by @KaifAhmad1):
|
||||
- Fixed `enable_named_graphs` handling so `TripletStore.execute_query()` now forwards `supports_named_graphs=False` when named-graph support is disabled in config.
|
||||
- Fixed duplicate dataset clause behavior in `QueryEngine.prepare_query()` so the same URI is not emitted as both `FROM <...>` and `FROM NAMED <...>`.
|
||||
- Added backward-compatible config alias support for `default_graph_uri` alongside existing `default_graph`.
|
||||
- Hardened graph URI handling in version-pruning `DROP SILENT GRAPH` updates by percent-encoding unsafe characters before SPARQL interpolation.
|
||||
- Added focused regression tests covering config-flag enforcement, duplicate clause prevention, `default_graph_uri` alias behavior, and pruning-path URI sanitization.
|
||||
- Verified with targeted feature tests: `tests/triplet_store/test_triplet_store.py` and `tests/change_management/test_managers.py` (54 passed).
|
||||
|
||||
- **ContextGraph Pagination & Edge Integrity Fixes** (PR #431 by @ZohaibHassan16, reviewed and patched by @KaifAhmad1):
|
||||
- **O(N) pagination bug** (`semantica/context/context_graph.py`): `find_nodes` and `find_edges` previously materialised the entire graph into a list before slicing — on a 50k-node / 100k-edge graph this allocated up to 2.5 million dicts per paginated request, starving the asyncio event loop and producing 502 Bad Gateway timeouts from the Vite proxy. Both methods now use generator expressions consumed via `itertools.islice(gen, skip, skip + limit)`, reducing time and space complexity from O(N) to O(limit) for the hot path.
|
||||
- **Ghost-node / "Nothing → Nothing" edge bug**: `add_edges` previously only accepted the `"source_id"` / `"target_id"` key names; edges serialised with `"source"` / `"target"` (the format emitted by `find_edges`) silently produced `None → None` edges that crashed the frontend physics engine. `add_edges` now accepts both naming conventions (`edge.get("source_id") or edge.get("source")`). A `continue` guard rejects any edge still missing either endpoint after the dual-key lookup.
|
||||
- **Deterministic pagination**: `find_nodes` and `find_active_nodes` now call `sorted()` on `node_type_index` sets before iterating, eliminating non-deterministic page boundaries caused by Python's unordered set iteration.
|
||||
- **`sorted()` TypeError** (review fix by @KaifAhmad1): the `sorted()` call filtered to `isinstance(nid, str)` entries only — previously a `None` or `int` node ID in the index caused an immediate `TypeError` crash on any type-filtered node query.
|
||||
- **`stats()` / pagination total mismatch** (review fix by @KaifAhmad1): `stats()` previously counted all entries in `self.nodes` and `self.edges` including structurally invalid ones that `find_nodes`/`find_edges` now silently skip. `stats()` applies the same validity filters (`n.node_id`, `e.source_id and e.target_id`) so that `node_count`, `edge_count`, `node_types`, and `edge_types` totals always match what the pagination methods can actually return — preventing the Explorer UI from computing phantom extra pages.
|
||||
- All 424 context tests pass, 0 regressions.
|
||||
|
||||
- **Security: CodeQL Alert Remediation** (PR by @KaifAhmad1, branch `security-enhancement`):
|
||||
- **Clear-text logging of sensitive information** (#6, #7 — CWE-312/359/532): Removed debug `print` blocks in `semantica/semantic_extract/relation_extractor.py` and `semantica/semantic_extract/triplet_extractor.py` that accessed and logged `method_options["api_key"]` (even partially masked). No sensitive data is now written to stdout in verbose mode.
|
||||
- **Incomplete URL substring sanitization** (#8 — CWE-20): Replaced `"http://a.com" in urls` in `tests/ingest/test_web_ingestor.py` with `any(url == "http://a.com" for url in urls)` — explicit exact equality per element, eliminating the ambiguous substring check that could match attacker-controlled URLs at arbitrary positions.
|
||||
- **Missing workflow permissions** (#1, #3 — least-privilege): Added `permissions: contents: read` at the workflow level in `.github/workflows/benchmark.yml` and `.github/workflows/security.yml`. Both workflows previously inherited repository-default permissions (potentially read-write); they only require read access to checkout code.
|
||||
|
||||
- **SKOS Vocabulary REST API & Hierarchy Engine** (PR #426 by @ZohaibHassan16):
|
||||
- Added `semantica/explorer/routes/vocabulary.py` with three endpoints: `GET /api/vocabulary/schemes` returns all `skos:ConceptScheme` nodes as `VocabularyScheme` dicts; `GET /api/vocabulary/hierarchy?scheme=<uri>` returns the full broader/narrower concept tree for a scheme using an O(V+E) in-memory adjacency-list algorithm with cycle detection via a visited set; `POST /api/vocabulary/import` accepts `.ttl`, `.rdf`, and `.owl` uploads, delegates parsing to `rdf_parser.parse_skos_file`, and ingests results into the active `GraphSession` via `add_nodes`/`add_edges`. Invalid files return HTTP 422.
|
||||
- Added `VocabularyScheme` and `ConceptNode` Pydantic models to `semantica/explorer/schemas.py`. `ConceptNode` is self-referential (`children: Optional[List['ConceptNode']]`) to support arbitrarily deep hierarchy trees.
|
||||
- All session calls offloaded via `asyncio.to_thread` to keep the event loop unblocked.
|
||||
- Added `tests/explorer/test_vocabulary.py` — 16 tests covering all three endpoints: scheme listing, metadata envelope fallback, empty graph, `broader`/`narrower`/`topConceptOf`/`hasTopConcept` edge directions, flat schemes, missing query params, cyclic edge safety, `.rdf`/`.owl` format paths, and invalid file 422 response. 99 total explorer tests passing, 0 regressions.
|
||||
- Depends on `semantica/explorer/utils/rdf_parser.py` introduced in PR #425.
|
||||
- **Explorer Server Integration & RDF Parsing Utility** (PR #425 by @ZohaibHassan16):
|
||||
- Added `semantica/explorer/utils/rdf_parser.py` — dedicated SKOS/RDF parsing utility using `rdflib`. Exposes `parse_skos_file(file_bytes, rdf_format)` which parses `.ttl` (Turtle) and `.rdf` (RDF/XML) files and returns a `(nodes, edges)` tuple of flat dicts compatible with `ContextGraph` ingestion. Extracts `skos:ConceptScheme` and `skos:Concept` nodes with a 3-priority label resolution strategy (exact `en` → `en-*` variants → untagged → any-language fallback → URI fragment). Collects all `skos:altLabel` values as a deduplicated list. Emits edges for all 6 SKOS structural predicates: `broader`, `narrower`, `inScheme`, `related`, `topConceptOf`, `hasTopConcept`. Edges pointing to external URIs not declared in the same file are silently dropped to avoid dangling references in the graph. Raises `ValueError` with a descriptive message on unparseable input.
|
||||
- Added `semantica/explorer/utils/__init__.py` — package initialiser for the new `utils` sub-package.
|
||||
- Updated `semantica/server.py` — mounts all Explorer API routers (`analytics`, `annotations`, `decisions`, `enrich`, `export_import`, `graph`, `temporal`) inside a graceful `try/except ImportError` block. The `vocabulary` router (pending #421) is guarded in its own isolated block so a missing module cannot prevent the existing routes from mounting. Both blocks log at `INFO`/`DEBUG` level rather than raising on absence.
|
||||
- Added `tests/explorer/test_rdf_parser.py` — 32 tests across 9 classes covering node/edge extraction, label priority, `altLabel` deduplication, all 6 SKOS edge types, orphan-edge filtering, empty graph, error cases, and RDF/XML format. 32 passed, 0 failures, 0 regressions against `tests/explorer/test_explorer_api.py` (51 tests).
|
||||
- Provides the necessary infrastructure for the upcoming `POST /api/vocabulary/import` endpoint tracked in #421.
|
||||
|
||||
- **SKOS Vocabulary Module** (PR #319 by @KaifAhmad1):
|
||||
- **Namespace helpers** (`semantica/ontology/namespace_manager.py`): Added `get_skos_uri(local_name)` — returns the full `http://www.w3.org/2004/02/skos/core#<local_name>` URI for any SKOS term. Added `build_concept_scheme_uri(name)` — slugifies a human-readable vocabulary name (spaces/special chars → hyphens, lower-cased) and anchors the result at the configured base URI as `<base>/vocab/<slug>`.
|
||||
- **Triplet-store SKOS helpers** (`semantica/triplet_store/triplet_store.py`): Added `add_skos_concept(concept_uri, scheme_uri, pref_label, alt_labels, broader, narrower, related, definition, notation)` — assembles and stores all required SKOS triples (auto-declares the `skos:ConceptScheme`, asserts `rdf:type skos:Concept`, `skos:inScheme`, `skos:prefLabel`, and all optional predicates) via the existing `add_triplets()` API; no new storage paths introduced. Added `get_skos_concepts(scheme_uri=None)` — issues a SPARQL `SELECT` via `execute_query()` and collapses multi-valued `altLabel`/`broader`/`narrower`/`related` bindings into structured concept dicts; optional `scheme_uri` restricts results to one vocabulary.
|
||||
- **OntologyEngine vocabulary APIs** (`semantica/ontology/engine.py`): Added three public methods that delegate to `QueryEngine` via `self.store.execute_query()` — `list_vocabularies()` returns all `skos:ConceptScheme` instances with labels; `list_concepts(scheme_uri)` returns every `skos:Concept` in a scheme with `pref_label` and `alt_labels`; `search_concepts(query, scheme_uri=None)` performs case-insensitive substring matching across `skos:prefLabel` and `skos:altLabel` with optional scheme scoping.
|
||||
- **Security**: `search_concepts` sanitises user input (escapes `\`, `"`, newlines) before embedding it in the SPARQL string literal. All URI interpolation uses the existing `_sanitize_uri` helper.
|
||||
- **Tests**: Added `TestSKOSOntologyEngine` (14 tests) to `tests/ontology/test_ontology_comprehensive.py` and `TestSKOSTripletStore` (6 tests) to `tests/triplet_store/test_triplet_store.py`. Coverage: URI helpers, vocabulary listing + deduplication, concept listing with multi-value alt-label collapse, search with/without scheme filter, injection sanitisation, empty results, and no-store error paths. 20 new tests, 0 failures, 1162 total passing, 0 regressions.
|
||||
- **Docs** (`docs/reference/ontology.md`): Added "SKOS Vocabulary Management" section with SKOS data-model reference table, `add_skos_concept` usage example, bulk import via rdflib + `add_triplets`, `list_vocabularies` / `list_concepts` / `search_concepts` usage examples, and `NamespaceManager` URI helper examples.
|
||||
- No new top-level Python package created; all code extends existing `semantica/ontology/` and `semantica/triplet_store/` packages. Fully opt-in and non-breaking.
|
||||
|
||||
- **SHACL Shape Generation & Validation** (PR #318 by @KaifAhmad1):
|
||||
- **Phase 1 — Generation**: Added `SHACLGenerator` to `semantica/ontology/ontology_generator.py` — 6-stage internal pipeline: `_build_class_index` → `_generate_node_shapes` → `_attach_property_shapes` → `_propagate_inheritance` → `_apply_quality_tier` → `serialize`. Derives SHACL node and property shapes from any Semantica ontology dict; zero hand-authoring. Three output formats: Turtle, JSON-LD, N-Triples. Three quality tiers: `"basic"` (structure + cardinality), `"standard"` (+ `sh:in`, `sh:pattern`, inheritance; default), `"strict"` (+ `sh:closed true` + `sh:ignoredProperties` on all non-empty shapes). Iterative inheritance propagation up to 3+ levels, cycle-safe (max 20 passes), no duplicate property shapes per shape. No-domain properties attach to all node shapes. Added `PropertyShape`, `NodeShape`, `SHACLGraph` dataclasses.
|
||||
- **Phase 1 — Engine API**: Added `OntologyEngine.to_shacl(ontology, *, format, base_uri, shapes_uri, include_inherited, severity, quality_tier, validate_output)` and `OntologyEngine.export_shacl(ontology, path, format, encoding)` to `semantica/ontology/engine.py`. Added `RDFExporter.export_shacl(shacl_string, file_path, format, encoding)` to `semantica/export/rdf_exporter.py` with extension validation (`.ttl`, `.jsonld`, `.nt`, `.shacl`).
|
||||
- **Phase 2 — Runtime Validation**: Added `SHACLViolation` (8 fields: `focus_node`, `result_path`, `constraint`, `severity`, `message`, `value`, `shape`, `explanation`; `to_dict()`) and `SHACLValidationReport` (`conforms`, `violations`, `warnings`, `infos`, `raw_report`; `violation_count`/`warning_count` properties; `summary()`, `explain_violations()`, `to_dict()`) to `semantica/ontology/ontology_validator.py`. Added `_run_pyshacl(data_graph_str, shacl_str, data_graph_format, shacl_format)` — thin wrapper around `pyshacl.validate()` returning typed `SHACLValidationReport`. `pyshacl` and `rdflib` are optional deferred imports (`pip install semantica[shacl]`); `ImportError` with install hint raised if absent. Added `OntologyEngine.validate_graph(data_graph, shacl=None, *, ontology=None, data_graph_format, shacl_format, explain, abort_on_first)` — exactly one of `shacl`/`ontology` must be provided (`ValueError` otherwise); `explain=True` populates plain-English explanations via rule-based templates for all 7 SHACL constraint types (`MinCount`, `MaxCount`, `Datatype`, `Class`, `In`, `Pattern`, `Closed`).
|
||||
- **Exports**: `SHACLGenerator`, `SHACLGraph`, `NodeShape`, `PropertyShape`, `SHACLValidationReport`, `SHACLViolation` added to `semantica/ontology/__init__.py`.
|
||||
- **Security & reliability fixes**:
|
||||
- **High** (`engine.py`): Replaced path-vs-content heuristic (`len < 500 and "\n" not in s`) with `os.path.exists()` — prevents attacker-controlled SHACL strings from being silently interpreted as file paths.
|
||||
- **High** (`ontology_generator.py`): `_propagate_inheritance` now uses `dataclasses.replace(pps)` instead of appending parent `PropertyShape` objects by reference — mutations on a child's inherited property no longer silently affect the parent.
|
||||
- **Medium** (`engine.py` / `ontology_validator.py`): Added `shacl_format` parameter to `validate_graph` and `_run_pyshacl`; full format alias map (`"ttl"→"turtle"`, `"jsonld"→"json-ld"`, `"ntriples"→"nt"`) in both `to_shacl` validate-output and `_run_pyshacl` — JSON-LD and N-Triples shapes no longer fail parsing.
|
||||
- **Medium** (`ontology_generator.py`): `sh:ignoredProperties` now emits full URI `<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>` instead of prefixed `rdf:type` — eliminates prefix-dependency in strict-tier Turtle output.
|
||||
- **Low** (`ontology_generator.py`): `_prefix_decls` now iterates `sorted(graph.prefixes.items())` — deterministic Turtle output for reproducible CI `git diff` checks.
|
||||
- **Tests**: Added `TestSHACLGeneration` (16 tests) to `tests/ontology/test_ontology_comprehensive.py` and `TestSHACLHierarchicalAndValidation` (18 tests) to `tests/ontology/test_ontology_advanced.py`. 34 new tests, 0 failures, 1111 total passing, 0 regressions.
|
||||
- **README**: Added `## Unreleased / Coming Next` section, SHACL bullet points under Features → Ontology and Export Formats, updated Modules table, full Phase 1 + Phase 2 code examples under `## Ontology`, `pip install semantica[shacl]` under Installation.
|
||||
|
||||
- **Temporal GraphRAG Integration** (PR #402 by @KaifAhmad1):
|
||||
- Added `TemporalGraphRetriever` to `semantica/context/context_retriever.py` — drop-in wrapper for any `ContextRetriever`; calls `base_retriever.retrieve(query)` then filters `related_entities`/`related_relationships` via `reconstruct_at_time()`; `at_time=None` is a true passthrough; returns new `RetrievedContext` objects via `dataclasses.replace()` (no in-place mutation); temporal modules guarded with `try/except` at import time.
|
||||
- Extended `ContextRetriever._generate_reasoned_response()` and `query_with_reasoning()` with `at_time` and `header_template` parameters — when `at_time` is set a structured temporal header (`[Graph context valid as of: … UTC | Source: KnowledgeGraph snapshot]`) is prepended to the LLM context block; omitted when `at_time=None` (prompt byte-identical to previous behaviour); naive datetimes normalised to UTC; header built via `str.replace` not `.format` (format-string injection guard).
|
||||
- Added `TemporalQueryRewriter` and `TemporalQueryResult` in `semantica/kg/temporal_query_rewriter.py` — extracts `temporal_intent` (`"before"`, `"after"`, `"at"`, `"during"`, `"between"`, `None`), `at_time`, `start_time`, `end_time`, and `rewritten_query` from natural-language queries; regex-only by default (zero LLM calls), optional LLM-assisted mode; datetime resolution always delegated to `TemporalNormalizer`; word-boundary guards prevent false matches (`at` inside `that`); year fallback handles noun-phrase dates like `"the 2021 merger"`; never calls `reconstruct_at_time`.
|
||||
- Exported `TemporalGraphRetriever` from `semantica.context`; exported `TemporalQueryRewriter`, `TemporalQueryResult` from `semantica.kg`.
|
||||
- **Security fixes**: format-string injection in header template (medium); unconditional temporal module import at package init (low).
|
||||
- **Bug fixes**: in-place mutation of `RetrievedContext` (high); naive datetime formatted without timezone (low); missing `timezone` import causing `NameError` (low).
|
||||
- Added 99 tests across `tests/context/test_temporal_retriever.py` (56) and `tests/kg/test_temporal_query_rewriter.py` (43); 0 failures, 0 regressions.
|
||||
|
||||
- **Temporal Provenance & Export** (PR #401 by @KaifAhmad1):
|
||||
- **Transaction time on provenance records** (`semantica/kg/provenance_tracker.py`): `track_entity()` now automatically attaches `recorded_at = datetime.now(UTC).isoformat()` to every new record — no opt-in required. Existing records without `recorded_at` continue to work in all existing query methods (treated as unknown, not an error). Added `query_recorded_between(start, end) -> list` returning all provenance records whose `recorded_at` falls within the inclusive range; accepts `datetime` objects or ISO strings including trailing `Z`.
|
||||
- **Fact revision audit trail** (`semantica/kg/provenance_tracker.py`): Added `revision_history(fact_id) -> list` returning the complete revision chain ordered by `recorded_at` ascending; each entry includes `version` (int, 1-based), `valid_from`, `valid_until`, `recorded_at`, `author`, and optionally `revision_type`/`supersedes`; returns `[]` for unknown facts (never raises). Added `export_audit_log(fact_ids, format) -> str` supporting `"json"` (pretty-printed) and `"csv"` (with header row) formats.
|
||||
- **OWL-Time RDF export** (`semantica/export/rdf_exporter.py`): `export_to_rdf()` gains `include_temporal: bool = False` and `time_axis: str = "valid"` parameters. When `include_temporal=True`, emits OWL-Time triples (`http://www.w3.org/2006/time#`) for every relationship carrying `valid_from`/`valid_until` — a `time:Interval` node linked via `time:hasTime`, `time:hasBeginning`/`time:hasEnd` with `time:Instant` nodes, and `time:inXSDDateTimeStamp` values. `time_axis` controls which axis is exported: `"valid"`, `"transaction"`, or `"both"`. Relationships without temporal metadata are unaffected. Default `include_temporal=False` produces output identical to current behavior. **Design decision for `TemporalBound.OPEN`**: OWL-Time has no standard predicate for "no known end date" — `time:hasEnd` is omitted and `semantica:openEndedInterval "true"^^xsd:boolean` is emitted on the interval node instead. Output parses without errors in rdflib.
|
||||
- **Stable snapshot serialization format** (`semantica/kg/temporal_query.py`, new `semantica/kg/schemas/temporal_snapshot_v1.json`): `create_snapshot()` now stamps `"format_version": "1.0"` on every snapshot. Added `validate_snapshot(snapshot) -> bool` — validates required fields (`format_version`, `label`, `timestamp`, `author`, `description`, `entities`, `relationships`, `checksum`); returns `False` with structured DEBUG-level error details on failure, never raises. Added `migrate_snapshot(snapshot) -> dict` — deep-copies and upgrades old-format snapshots to v1.0, populating missing required fields with `None`; already-v1.0 snapshots returned unchanged with no data loss. New `semantica/kg/schemas/temporal_snapshot_v1.json` — JSON Schema (draft 2020-12) defining required and optional fields, types, and constraints.
|
||||
- Added 28 new tests in `tests/test_401_temporal_provenance_export.py` covering every acceptance criterion; 451 related tests pass, 0 regressions.
|
||||
|
||||
- **Temporal Metadata Extraction from Text** (PR #400 by @KaifAhmad1):
|
||||
- Added `extract_temporal_bounds: bool = False` parameter to `extract_relations_llm()`. When `True`, the LLM prompt is extended with a calibrated confidence scale and four few-shot examples; each returned `Relation` gains `valid_from`, `valid_until`, `temporal_confidence` (0.0–1.0), and `temporal_source_text` in its `metadata` dict. Default `False` preserves 100% backward compatibility.
|
||||
- Confidence scale anchors baked into the prompt: `1.00` = full ISO date, `0.90` = year+month, `0.85` = year only, `0.75` = quarter, `0.65` = named season/approximate range, `0.50` = vague relative with computable anchor, `0.35` = highly vague, `0.00` = no temporal signal. LLMs self-report certainty rather than clustering near 1.0.
|
||||
- Low temporal confidence (< 0.5) with a non-null date logs a `WARNING`; signal is never suppressed — callers decide how to filter.
|
||||
- Cache key now includes the `extract_temporal_bounds` flag to prevent cross-mode cache pollution.
|
||||
- Flag propagated through `_extract_relations_chunked()` so long-text chunked extraction also carries temporal metadata.
|
||||
- Added `RelationWithTemporalOut` and `RelationsWithTemporalResponse` Pydantic schemas in `semantica/semantic_extract/schemas.py`. A separate schema is required because `RelationOut` uses `extra="ignore"`, which silently drops any undeclared field including the four temporal fields.
|
||||
- New `semantica/kg/temporal_normalizer.py` — `TemporalNormalizer` class (zero LLM calls, pure regex + `dateutil` arithmetic):
|
||||
- `normalize(value)` → `(valid_from, valid_until)` UTC `datetime` tuple or `None`. Resolution order: ISO 8601 full parse → partial-date regex (year-only, month+year, YYYY-MM, Q[1-4] YYYY) → ambiguous-slash-date detection → domain phrase map → relative phrase resolution via `relativedelta`.
|
||||
- `normalize_phrase(phrase)` → metadata dict `{"maps_to": ..., "type": ..., "domain": [...]}` or `None` — exact match then regex-pattern keys.
|
||||
- Ambiguous `DD/MM/YYYY`-style inputs issue `TemporalAmbiguityWarning` and return `None` — never silently guesses locale.
|
||||
- Unparseable inputs return `None` with a debug log — never raise.
|
||||
- Relative phrases (`"last year"`, `"three months ago"`, etc.) raise `ValueError` if `reference_date` is `None` rather than guessing.
|
||||
- Default phrase map covers 13 domains: General/Policy (`effective date`, `effective from/as of/beginning`, `in force until`, `retroactive to`, `sunset clause`), Healthcare (`approval date`, `expiry date`, `market authorization`), Cybersecurity (`incident window`, `campaign period`), Supply Chain (`certification valid through`), Finance (`trading halt`), Energy (`commissioned date`, `decommissioned date`).
|
||||
- User-supplied `phrase_map` is merged over defaults at construction (`{**defaults, **user_map}`) — custom entries win without forking the library.
|
||||
- Added `TemporalAmbiguityWarning(UserWarning)` to `semantica/utils/exceptions.py`.
|
||||
- Exported `TemporalNormalizer` from `semantica/kg/__init__.py`.
|
||||
- Added 53 new tests in `tests/semantic_extract/test_temporal_extraction.py`; zero real LLM calls, suite runs in ~3.5 s. All 873 existing tests continue to pass.
|
||||
|
||||
- **Fix: OllamaProvider ignores `base_url`** (PR #408 by @AlexeyMyslin, fixed by @KaifAhmad1):
|
||||
- `OllamaProvider._init_client()` was assigning the raw `ollama` module to `self.client` instead of instantiating `ollama.Client(host=self.base_url)`, causing all requests to silently hit `localhost:11434` regardless of the `base_url` passed by the user
|
||||
- Fixed by replacing `self.client = ollama` with `self.client = ollama.Client(host=self.base_url)` — remote Ollama servers (e.g. `http://192.168.1.3:11434`) are now reachable
|
||||
- Added 3 regression tests: default URL forwarded as host, custom URL forwarded as host, and guard ensuring `self.client` is never the raw module
|
||||
|
||||
- **Temporal Awareness in Context Graph** (PR #399 by @KaifAhmad1):
|
||||
- Added `valid_from` and `valid_until` fields to the `Decision` dataclass and `record_decision()` — decisions now carry explicit validity windows; superseded decisions remain in the graph (history is immutable)
|
||||
- Added `include_superseded=False` and `as_of=None` parameters to `find_precedents_by_scenario()` — defaults exclude expired decisions; `as_of` enables point-in-time precedent queries
|
||||
- Added `ContextGraph.state_at(timestamp)` — returns a serializable point-in-time snapshot of all nodes, edges, and decisions whose validity windows include `timestamp`; source graph is never mutated
|
||||
- Stamped `recorded_at` on causal relationship edges created via `add_causal_relationship()` — enables transaction-time filtering
|
||||
- Added `CausalChainAnalyzer.trace_at_time(event_id, at_time)` — reconstructs a causal chain using only edges recorded up to `at_time` (transaction time); returns an empty list when `at_time` predates all facts, never raises
|
||||
- Added `AgentContext.checkpoint(label)`, `diff_checkpoints(label1, label2)`, and `flush_checkpoint(label)` — named in-memory context snapshots with structured diffs (`decisions_added`, `decisions_removed`, `relationships_added`, `relationships_removed`) and optional persistence via `TemporalVersionManager`
|
||||
- **Review fixes applied in the same PR**:
|
||||
- Fixed `max_depth` error message in `trace_at_time` to match actual bound (1–100)
|
||||
- Fixed Cypher `at_time` query parameter to RFC3339 UTC (`Z` suffix) for unambiguous external DB comparisons
|
||||
- `_normalize_temporal_input` now raises `ValueError` on unparseable strings instead of silently returning raw input
|
||||
- Replaced `datetime.now()` with `datetime.utcnow()` for all `recorded_at` and checkpoint timestamps — aligns with codebase convention and avoids wrong local time on Windows
|
||||
- `flush_checkpoint` wraps `TemporalVersionManager()` construction in a `try/except` and re-raises as `RuntimeError` with a clear actionable message
|
||||
- Added 7 new tests (93 total across context modules, 0 failures)
|
||||
|
||||
- **spaCy Runtime Fallback for NER Benchmarks**:
|
||||
- Hardened `NERExtractor` spaCy initialization so installed-but-broken spaCy environments no longer crash during extractor construction.
|
||||
- Updated ML entity extraction fallback behavior to catch runtime spaCy initialization failures, not just missing-model errors.
|
||||
- Added regression coverage for the "spaCy present but unusable at runtime" initialization path.
|
||||
- **Deterministic Temporal Reasoning Engine** (PR #398 by @KaifAhmad1, implemented and follow-up fixes by OpenAI Codex):
|
||||
- Added `semantica.kg.temporal_reasoning` as the single source of truth for deterministic, LLM-free temporal reasoning with an explicit zero-LLM module contract
|
||||
- Implemented `TemporalInterval`, full Allen interval algebra via `IntervalRelation`, and `TemporalReasoningEngine`
|
||||
- Added deterministic helpers for interval overlap/containment checks, open-ended activity checks, interval merging, gap analysis, coverage calculation, timelines, retroactive coverage, and temporal normalization
|
||||
- Integrated temporal query interval logic with the reasoning engine in `TemporalGraphQuery`
|
||||
- Preserved `semantica.reasoning` access via re-exports without making it the canonical implementation source
|
||||
- Fixed open-ended `query_time_range(..., end_time=None)` handling so temporal range queries no longer crash on `TemporalBound.OPEN`
|
||||
- Restored `temporal_granularity` behavior for point-in-time checks in `query_at_time()`
|
||||
- Eliminated the `semantica.reasoning` / `semantica.kg` circular import risk introduced during the initial module move
|
||||
- Added regression coverage for all 13 Allen relations, open-ended intervals, month-granularity point queries, open-ended range queries, retroactive coverage, and normalization idempotence
|
||||
|
||||
- **Temporal Query Engine: Point-in-Time Correctness** (PR #397 by @KaifAhmad1, implemented and follow-up fixes by OpenAI Codex):
|
||||
- Added `reconstruct_at_time(graph, at_time)` to `TemporalGraphQuery` to build a self-consistent point-in-time subgraph without mutating the input graph
|
||||
- Updated `query_at_time()` to use point-in-time reconstruction internally so returned subgraphs exclude dangling edges when entity lifetimes are available
|
||||
- Added `TemporalConsistencyIssue` and `TemporalConsistencyReport` plus temporal consistency validation for:
|
||||
- inverted relationship intervals
|
||||
- relationships outside entity lifetimes
|
||||
- missing source/target entities
|
||||
- overlapping same-type relationships on the same edge
|
||||
- temporal gaps where a fact ends and restarts later
|
||||
- Added a module-level `validate_temporal_consistency(graph)` API alongside the query-engine method
|
||||
- Implemented sequence and cycle pattern detection with structured outputs containing `pattern_type`, `signature`, `frequency`, and per-occurrence node/edge/time details
|
||||
- Implemented calendar-aligned temporal evolution bucketing based on `temporal_granularity`
|
||||
- Added causal ordering controls to `find_temporal_paths()` via `enforce_causal_ordering` and `ordering_strategy` (`strict`, `overlap`, `loose`)
|
||||
- **Follow-up fixes applied in the same PR**:
|
||||
- Made `validate_temporal_consistency()` non-throwing on malformed temporal fields and return report errors instead of raising
|
||||
- Enforced exclusive `valid_until` semantics for point-in-time checks (`valid_from <= at_time < valid_until`)
|
||||
- Kept `query_time_range(..., temporal_aggregation="evolution")` backward-compatible by returning the flat relationship list plus a new `relationship_buckets` field
|
||||
- Hardened temporal pattern detection for open-ended intervals (`TemporalBound.OPEN`) to avoid datetime arithmetic/comparison crashes
|
||||
- Normalized relationship endpoints during point-in-time reconstruction so mixed-type IDs like `1` and `"1"` do not silently drop valid edges
|
||||
- Added in-code design comments documenting the sequence/cycle output structure required by the checklist
|
||||
- Added and expanded regression coverage for point-in-time reconstruction, exclusive end bounds, non-throwing validation, module-level validator access, pattern detection with gap tolerance/open bounds, evolution bucketing, causal ordering, and mixed-type IDs
|
||||
|
||||
- **Core Temporal Data Model Overhaul** (PR #396 by @KaifAhmad1, implemented and follow-up fixes by OpenAI Codex):
|
||||
- Added `semantica.kg.temporal_model` with shared helpers for parsing, normalizing, serializing, and deserializing temporal relationship fields
|
||||
- Exported `TemporalBound` and `BiTemporalFact` from `semantica.kg` for backward-compatible temporal relationship handling
|
||||
- Updated `TemporalGraphQuery` to use shared temporal parsing/model helpers instead of ad hoc string handling
|
||||
- Added support for `valid`, `transaction`, and `both` time axes in temporal query filtering
|
||||
- Standardized temporal normalization on `timezone.utc` for better cross-version portability
|
||||
- Added `TemporalValidationError` to utils exports and made invalid temporal inputs consistently raise it
|
||||
- Added history-preserving temporal revisions in `TemporalVersionManager.apply_revision()` with provenance metadata and supersession semantics
|
||||
- Added safer snapshot persistence by serializing revision metadata before storage and surfacing storage failures as `ProcessingError`
|
||||
- **Follow-up fixes applied in the same PR**:
|
||||
- Added a default factory for `BiTemporalFact.recorded_at` and preserved legacy transaction-axis behavior by falling back to `valid_from` when `recorded_at` is missing
|
||||
- Treated `TemporalBound.OPEN` as an unbounded value in shared query parsing so open-ended facts do not fail in public APIs like `analyze_evolution()` and path filtering
|
||||
- Recomputed snapshot checksums before persisting revised snapshots and any original snapshot inserted during revision flow
|
||||
- Replaced second-based revision suffixes with collision-resistant revision IDs/labels to avoid duplicate save failures under rapid revisions
|
||||
- Removed warning spam caused by canonical serialized open bounds represented as `None`
|
||||
- Added and expanded regression coverage for UTC normalization, transaction-axis queries, open-ended bounds, revision integrity, checksum verification, and collision-resistant revision identifiers
|
||||
|
||||
- **Audit Trail, Named Tags, and Rollback Protection** (PR #394 by @ZohaibHassan16, reviewed by @KaifAhmad1, follow-up fixes by OpenAI Codex):
|
||||
- Added mutation-level audit tracking for `ContextGraph` node and edge changes via `TemporalVersionManager.attach_to_graph()` and persistent mutation logging backends
|
||||
- Added named version tags in both in-memory and SQLite storage so human-readable tags can point to saved snapshots
|
||||
- Added rollback protection to `restore_snapshot()` so destructive graph restores require explicit confirmation
|
||||
- Added `get_node_history()` for per-entity audit inspection and `diff()` as a Git-like alias over version comparisons
|
||||
- Preserved backward compatibility for snapshot payloads and diff outputs by supporting both `nodes`/`edges` and `entities`/`relationships`
|
||||
- Fixed mixed-schema snapshot comparison and version metadata counts after the audit-trail feature landed on top of PR #393
|
||||
- Fixed restore replay so rollback does not generate synthetic mutation events in the audit log
|
||||
- Added version-label assignment for previously unlabeled mutations when a snapshot is created
|
||||
- Resolved merge conflicts against updated `main` in `managers.py`, `version_storage.py`, `context_graph.py`, and `test_managers.py`
|
||||
- Added and updated regression coverage for audit history, rollback safety, version-label persistence, and snapshot compatibility
|
||||
|
||||
- **Snapshot Schema Compatibility Fix** (PR #393 by @ZohaibHassan16, reviewed by @KaifAhmad1, follow-up fixes by OpenAI Codex):
|
||||
- Fixed silent snapshot restore failures caused by the `ContextGraph` `nodes`/`edges` schema not matching the version manager's legacy `entities`/`relationships` expectations
|
||||
- Updated temporal snapshot handling to accept both `nodes`/`edges` and `entities`/`relationships`
|
||||
- Preserved both schema shapes in stored snapshots to maintain backward compatibility during migration
|
||||
- Fixed temporal diffing and detailed comparison paths so new-format and mixed-format snapshots compare correctly
|
||||
- Fixed version metadata counts so `entity_count` and `relationship_count` remain accurate for both snapshot schemas
|
||||
- Restored ontology snapshot compatibility fields removed during the PR follow-up iteration
|
||||
- Added regression coverage for new-format snapshot creation, metadata counts, and mixed-schema diffing
|
||||
|
||||
- **ContextGraph Traversal Fallbacks for DecisionQuery & DecisionRecorder** (PR #386 by @ZohaibHassan16, reviewed and fixed by @KaifAhmad1):
|
||||
- Added native `ContextGraph` fallback execution paths to all 7 `DecisionQuery` methods (`_find_precedents_basic`, `find_by_category`, `find_by_entity`, `find_by_time_range`, `multi_hop_reasoning`, `trace_decision_path`, `find_similar_exceptions`) — resolves issue #379 where hardcoded Cypher queries broke in-memory usage
|
||||
- Added native `ContextGraph` fallback paths to 4 `DecisionRecorder` methods (`link_entities`, `record_exception`, `link_precedents`, `_store_decision_node`, `_store_exception_node`) using `add_node` / `add_edge` primitives
|
||||
- Implemented undirected BFS in `multi_hop_reasoning` fallback — traverses both outgoing and incoming edges so decisions are reachable from linked entities (matches Cypher `(start)-[*1..N]-(d:Decision)` semantics)
|
||||
- Fixed `isinstance(graph_store, ContextGraph)` guards → `type(graph_store) is ContextGraph` — prevents `Mock(spec=ContextGraph)` from triggering fallback branches and breaking 2 existing tests
|
||||
- Fixed `add_node(properties=metadata)` call in `_store_decision_node` and `_store_exception_node` — changed to `**metadata` so all decision fields are stored flat and remain readable via `_dict_to_decision`; previous form silently nested every field under a `"properties"` key
|
||||
- Fixed spurious `properties={}` keyword argument in all `add_edge` fallback calls — argument did not match the actual `add_edge(**properties)` signature
|
||||
- Fixed tz-aware / naive `datetime` mismatch in `find_by_time_range` fallback — strips `tzinfo` from aware bounds when stored timestamps are naive, preventing `TypeError` at comparison time
|
||||
- Hoisted `find_edges()` calls out of the BFS `while` loop in `trace_decision_path` — edges are now fetched once per call instead of once per visited node, eliminating O(nodes × total_edges) repeated full-graph scans
|
||||
- Removed duplicate `from ..embeddings import EmbeddingGenerator` import in `decision_query.py`
|
||||
- Added `tests/context/test_decision_query_fallback.py` with 14 tests: full integration test covering the complete fallback flow end-to-end, plus 13 targeted unit tests covering each `DecisionQuery` and `DecisionRecorder` fallback method individually, tz-aware/naive datetime mixing, and `Mock` guard correctness
|
||||
|
||||
- **ContextGraph Thread Safety & Pagination** (PR #385, Issues #378 #376 by @ZohaibHassan16, review & fixes by @KaifAhmad1):
|
||||
- `ContextGraph`: added `threading.RLock` (`self._lock`) to `__init__`; all mutation paths (`add_nodes`, `add_edges`, `add_node`, `add_edge`, `save_to_file`, `load_from_file`, `link_graph`) and all read/query paths (`find_nodes`, `find_edges`, `find_node`, `find_active_nodes`, `get_neighbors`, `query`, `stats`, `density`) now protected with `with self._lock:` to prevent race-condition corruption under concurrent FastAPI workers
|
||||
- `find_nodes` and `find_edges` gained native `skip`/`limit` pagination parameters so the explorer layer never loads the full collection into memory to slice it
|
||||
- `GraphSession` (`session.py`): introduced session-level `RLock` wrapping all graph access; all 8 lazy analytics properties (`centrality`, `community`, `connectivity`, `path_finder`, `node_embedder`, `similarity`, `link_predictor`, `validator`) initialised under the lock (thread-safe double-checked); `get_nodes()` and `get_edges()` delegate pagination to the graph layer when no in-memory filter is needed
|
||||
- `pyproject.toml`: removed duplicate entry and added missing comma in the `all` optional-dependency array that caused `ERROR Failed to parse pyproject.toml: Unclosed array` in CI
|
||||
- **Fixes applied post-review (by @KaifAhmad1)**:
|
||||
- Fixed `/api/graph/search` returning empty `content` and `properties` — `ContextGraph.query()` wraps results in `node.to_dict()` which uses a `"properties"` envelope, but `_node_dict_to_response` expected a flat `{id, type, content, metadata}` shape; `session.search()` now normalises the envelope before returning
|
||||
- Fixed edge metadata silently dropped on import — `add_edges()` read only from the `"properties"` key, but edges produced by `find_edges()` and `build_graph_dict()` use `"metadata"`; fixed with `edge.get("properties") or edge.get("metadata", {})` fallback
|
||||
- Fixed `POST /api/enrich/links` blocking the asyncio event loop — the O(n) `score_link` scoring loop ran inline in the `async` handler; wrapped in `asyncio.to_thread(_score_all)`
|
||||
- Removed merge-artifact dead code in `session.py`: duplicate `self.annotations` assignment, duplicate un-locked property set, and double-query logic in `get_nodes()`/`get_edges()` that recomputed results outside the lock and threw away the correctly-paginated result computed inside it
|
||||
- Removed merge-artifact dead code in `enrich.py`: unreachable second `predict_links` implementation block after early `return`, and duplicate `nodes, _` fetch in `detect_duplicates`
|
||||
|
||||
- **Knowledge Explorer API Backend** (PR #384, Issue #377 by @ZohaibHassan16, review & fixes by @KaifAhmad1):
|
||||
- Added `semantica.explorer` package — a full FastAPI backend for the Semantica Knowledge Explorer dashboard
|
||||
- `app.py`: `create_app(session)` factory with CORS middleware, custom exception handlers (`KeyError→404`, `ValueError→422`), and HTML5 static-file fallback routing; generic `Exception` handler correctly re-raises `HTTPException` so dependency-injection 503s are not swallowed
|
||||
- `session.py`: `GraphSession` — thread-safe container wrapping a `ContextGraph` with 8 lazily-initialised analytics components (`CentralityCalculator`, `CommunityDetector`, `ConnectivityAnalyzer`, `PathFinder`, `NodeEmbedder`, `SimilarityCalculator`, `LinkPredictor`, `GraphValidator`); all lazy properties initialised under `RLock` to prevent double-instantiation under concurrent requests; shared `build_graph_dict(node_ids=None)` method eliminates duplication across route files; `from_file(path)` classmethod loads from JSON
|
||||
- `ws.py`: `ConnectionManager` — thread-safe WebSocket manager with `connect()`, `disconnect()`, `broadcast(event_type, data)`, and `send_personal()` support; safe disconnection cleanup during broadcast
|
||||
- `dependencies.py`: `get_session(request)` and `get_ws_manager(request)` FastAPI `Depends`-compatible callables; `get_session` raises `HTTP 503` when no session is attached
|
||||
- 7 modular route files, all using `asyncio.to_thread` for sync graph operations:
|
||||
- `routes/graph.py`: `GET /api/graph/nodes` (type/keyword filter, pagination), `GET /api/graph/node/{id}`, `GET /api/graph/node/{id}/neighbors` (BFS, depth 1–5), `GET /api/graph/edges` (type/source/target filter), `GET /api/graph/node/{id}/path` (BFS or Dijkstra — algorithm param now correctly dispatched), `POST /api/graph/search`, `GET /api/graph/stats`
|
||||
- `routes/analytics.py`: `GET /api/analytics` (centrality, community, connectivity — comma-separated metrics param), `GET /api/analytics/validation`
|
||||
- `routes/decisions.py`: `GET /api/decisions` (category filter, pagination), `GET /api/decisions/{id}`, `GET /api/decisions/{id}/chain` (BFS causal chain up to 5 hops), `GET /api/decisions/{id}/precedents` (category + scenario keyword ranking), `GET /api/decisions/{id}/compliance` (in-graph check over `violates`/`non_compliant`/`breaches` edges — no longer a stub)
|
||||
- `routes/temporal.py`: `GET /api/temporal/snapshot` (ISO-8601 `at` param), `GET /api/temporal/diff` (added/removed node sets between two timestamps), `GET /api/temporal/patterns` (graceful fallback when `TemporalPatternDetector` unavailable, with warning log for unexpected errors)
|
||||
- `routes/enrich.py`: `POST /api/enrich/extract` (NLP entity/relation extraction), `POST /api/enrich/links` (per-node link prediction via `score_link` against all non-adjacent candidates — fixed from broken `predict_links` call), `POST /api/enrich/dedup` (duplicate detection — fixed missing `asyncio.to_thread` that was blocking the event loop), `POST /api/reason` (forward/backward inference via `Reasoner`)
|
||||
- `routes/export_import.py`: `POST /api/export` (12 formats: JSON, Turtle, RDF-XML, N-Triples, CSV, GraphML, GEXF, OWL, Cypher, AQL, YAML — temp file always cleaned up via `try/finally`), `POST /api/import` (JSON/JSON-LD multipart upload with WebSocket progress events)
|
||||
- `routes/annotations.py`: `GET /api/annotations`, `POST /api/annotations` (validates node exists; `add_annotation` mutates dict in-place so no extra roundtrip), `DELETE /api/annotations/{id}`
|
||||
- `schemas.py`: 28 Pydantic v2 request/response models covering all endpoint shapes including pagination, temporal, enrichment, compliance, and annotation types
|
||||
- `__init__.py`: `semantica-explorer` CLI entry point — `--graph`, `--host`, `--port`, `--no-browser` args; validates graph file exists; checks for `uvicorn`; opens browser after 1.5 s delay
|
||||
- `pyproject.toml`: added `[project.optional-dependencies] explorer` group (`fastapi`, `uvicorn[standard]`, `websockets`, `python-multipart`); registered `semantica-explorer` script entry point; fixed missing comma in `all` extra that broke `pip install semantica[all]`
|
||||
- **Fixes applied post-review (by @KaifAhmad1)**:
|
||||
- Fixed `predict_links` endpoint — was calling `predictor.predict_links(graph_dict, node_id, top_n=...)` with wrong type (`dict` as `graph_store`), wrong positional arg (`node_id` as `node_labels`), and wrong kwarg (`top_n` vs `top_k`); rewrote to iterate all non-adjacent candidate nodes and call `predictor.score_link(session.graph, source, candidate)` directly
|
||||
- Fixed `detect_duplicates` endpoint — `session.get_nodes()` was called directly in an `async def` handler without `asyncio.to_thread`, blocking the event loop
|
||||
- Fixed temp file leak in `export_graph` — file was not deleted on exception from `export_fn` or `open()`; wrapped in `try/finally`; moved `import os` to module level
|
||||
- Fixed `pyproject.toml` `all` extra — two consecutive strings with no comma between them caused a TOML syntax error
|
||||
- Fixed generic `Exception` handler swallowing `HTTPException(503)` raised by `get_session`
|
||||
- Fixed compliance endpoint — imported `PolicyEngine` then discarded it, always returning `compliant=True`; replaced with in-graph edge scan
|
||||
- Fixed `temporal_patterns` bare `except Exception` silently hiding bugs — split into `ImportError` (silent graceful) and `Exception` (warning log)
|
||||
- Fixed all 8 lazy analytics properties to initialise under `_lock` (thread-safe double-checked)
|
||||
- Fixed `find_path` ignoring the `algorithm` query param — now dispatches to `dijkstra_shortest_path` or `bfs_shortest_path`
|
||||
- Removed unnecessary `get_annotations()` round-trip in `create_annotation`
|
||||
- Removed `import traceback` unused import in `app.py`
|
||||
- Deduplicated `_build_graph_dict` (was copied identically in `graph.py`, `analytics.py`, `export_import.py`) into `GraphSession.build_graph_dict()`
|
||||
- 49 integration tests in `tests/explorer/test_explorer_api.py` using `starlette.testclient.TestClient` — all passing; covers health, nodes, edges, search, stats, decisions, causal chains, precedents, compliance (including violation detection), temporal snapshots/diff/patterns, analytics, reasoning, entity extraction, link prediction, deduplication, annotations, export (JSON + node-subset), and import (JSON + edges + unsupported format)
|
||||
- **Reasoning Dead Code Removal** (PR #387, Issue #382 by @ZohaibHassan16):
|
||||
- Removed lines 357–358 in `semantica/reasoning/reasoner.py` that silently overwrote the sophisticated `_match_pattern` regex (which handles pre-bound variable embedding, repeated-variable backreferences via `(?P=var)`, and non-greedy named capture groups) with a simpler `re.escape`-based pattern, making all the prior logic unreachable dead code
|
||||
- Removed duplicate unreachable `return None` on line 368 (syntactically dead, appearing immediately after another `return None` in the same branch)
|
||||
- Surfaced `re.error` exceptions instead of swallowing them with `except Exception: pass`, preventing silent failures when malformed patterns were passed to `re.match`
|
||||
- Before this fix, any rule using the same variable twice (e.g. `rel(?x, ?x)`) generated a duplicate named group error that was silently caught, causing the match to return `None` regardless of the fact — breaking transitivity, symmetry, and self-join rule patterns entirely
|
||||
|
||||
- **Agno Agentic Framework Integration** (Issue #249):
|
||||
- Added `AgnoContextStore` — graph-backed agent memory implementing the `agno.memory.db.base.MemoryDb` protocol; wraps `AgentContext` + `VectorStore`; supports `create()`, `table_exists()`, `memory_exists()`, `read_memories()`, `upsert_memory()`, `delete_memory()`, `drop_table()`, `clear()` plus extended `record_decision()`, `find_precedents()`, `retrieve()` methods
|
||||
- Added `AgnoKnowledgeGraph` — multi-hop GraphRAG knowledge base implementing `agno.knowledge.base.AgentKnowledge`; ingests files, directories, URLs, and raw text via NER → relation extraction → graph build → vector index pipeline; `search()` returns `AgnoDocument` objects; `get_graph_context(entity)` returns text summary of entity's graph neighbourhood
|
||||
- Added `AgnoDecisionKit` — Agno `Toolkit` subclass exposing 6 decision-intelligence tools: `record_decision`, `find_precedents`, `trace_causal_chain`, `analyze_impact`, `check_policy`, `get_decision_summary`
|
||||
- Added `AgnoKGToolkit` — Agno `Toolkit` subclass exposing 7 KG pipeline tools: `extract_entities`, `extract_relations`, `add_to_graph`, `query_graph`, `find_related`, `infer_facts`, `export_subgraph`
|
||||
- Added `AgnoSharedContext` — team-level coordinator with a single shared `ContextGraph`; `bind_agent(role)` returns a role-scoped `_AgentScopedStore` with cross-agent memory visibility; thread-safe via `RLock`
|
||||
- All 5 components degrade gracefully when `agno` is not installed (`AGNO_AVAILABLE` flag); importable and functional without agno present
|
||||
- Added `agno = ["agno>=1.0.0"]` optional dependency in `pyproject.toml`; included in `all` extra
|
||||
- 110 integration tests in `tests/integrations/agno/` covering all public APIs, MemoryDb protocol compliance, GraphRAG search, tool registration, shared memory isolation, and thread-safety
|
||||
- 3 cookbook notebooks in `cookbook/integrations/`: `agno_decision_intelligence.ipynb` (loan underwriting), `agno_graphrag_context.ipynb` (regulatory compliance), `agno_multi_agent_shared_context.ipynb` (multi-agent team coordination)
|
||||
- Full reference documentation in `docs/integrations/agno.md`
|
||||
|
||||
- **Novita AI Provider** (PR #374 by @Alex-wuhu):
|
||||
- Added `NovitaProvider` — OpenAI-compatible integration via `https://api.novita.ai/v1`; supports `generate()` and `generate_structured()` (JSON forced format)
|
||||
- Default model: `deepseek/deepseek-v3.2`; configurable via `NOVITA_API_KEY` environment variable
|
||||
- Registered `"novita"` in the built-in provider factory; usable via `create_provider("novita")`
|
||||
- Added integration tests in `tests/test_novita_integration.py` with proper assertions and graceful skip when `NOVITA_API_KEY` is unset
|
||||
|
||||
- **Native Datalog Reasoning Engine** (PR #371, Issue #368 by @ZohaibHassan16, reviewed and fixed by @KaifAhmad1):
|
||||
- Added `DatalogReasoner` to `semantica.reasoning` — a pure-Python, bottom-up semi-naive fixpoint engine with guaranteed termination on finite graphs
|
||||
- Supports recursive Horn clause rules (e.g. `ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).`) that existing engines loop on indefinitely
|
||||
- Memory-optimized `_unify()` with deferred dict allocation — zero allocation on failed unifications
|
||||
- `O(1)` delta-index lookup per iteration eliminates redundant `O(N)` rule re-evaluations in semi-naive loop
|
||||
- `query("pred(?X, ?Y)")` returns variable-binding dicts; supports both uppercase `?Y` and lowercase `?y` variable syntax
|
||||
- `query(..., bindings={"Y": "val"})` pre-binds variables for exact-match verification
|
||||
- `load_from_graph(ContextGraph)` converts all edges and nodes to Datalog facts in one call; handles both `find_edges`/`find_nodes` and raw `edges`/`nodes` graph APIs
|
||||
- `add_fact()` accepts `"pred(a, b)"` strings and Semantica dicts (`subject/predicate/object`, `source/target/type`, `type/id` shapes); warns on unrecognised dict format instead of silently dropping
|
||||
- `_derived` cache flag — `derive_all()` skips re-evaluation when no facts or rules have changed since last run; `query()` respects the cache
|
||||
- Progress tracking wrapped in `try/finally` — `stop_tracking()` always called even on exception
|
||||
- `DatalogReasoner`, `DatalogFact`, `DatalogRule` exported from `semantica.reasoning`
|
||||
- 18 tests covering recursive rules, multi-hop inference, variable binding, graph integration, idempotency, and edge cases — all passing
|
||||
- **Ontology Diff & Migration** (PR #367 by @ZohaibHassan16, review & fixes by @KaifAhmad1):
|
||||
- `VersionManager.diff_ontologies(base, target)` — structured diff between two ontology dicts using hash-map lookups; handles URI-less items via `name` fallback; deep equality checks for unordered lists; now covers classes, properties, individuals, and axioms
|
||||
- `ChangeLogAnalyzer.analyze(diff)` — classifies each change by semantic impact: removed classes/properties → `CRITICAL/BREAKING`; narrowed domain/range/cardinality → `HIGH/BREAKING`; hierarchy modifications → `MEDIUM/POTENTIALLY_BREAKING`; added elements and annotation updates → `INFO/NON_BREAKING`
|
||||
- `ImpactReport` dataclass and `generate_change_report(diff)` public helper — returns a structured dict with `summary`, `impact_classification` (breaking / potentially_breaking / safe), `recommendations`, and the raw `diff`
|
||||
- `OntologyEngine.compare_versions(base_id, target_id, **options)` — end-to-end orchestrator: loads versions from `VersionManager`, runs `diff_ontologies`, generates impact report; accepts `base_dict`/`target_dict` overrides to bypass version store; `run_validation=True` triggers `OntologyValidator` on the target schema; `graph_data=...` additionally runs `GraphValidator` on instance data against the new schema
|
||||
- `OntologyEngine.get_ontology_version_dict(version_id)` — utility to load a registered version as a plain dict ready for diffing
|
||||
- Documentation added to `docs/reference/change_management.md`: "Ontology Diff & Migration" section with code example and full report format reference
|
||||
- 7 tests added to `tests/change_management/test_managers.py` covering: empty diff, unordered list equality, URI/name fallback, breaking class removal, narrowed domain (HIGH), safe additions and annotation changes, `compare_versions` dict override, version-not-found error path, individuals/axioms diff coverage, null constraint value flagged as breaking
|
||||
- **Fixes applied post-review (by @KaifAhmad1)**:
|
||||
- Fixed typo in `ChangeCategory` enum value: `"potenitally_breaking"` → `"potentially_breaking"`
|
||||
- Fixed missing space in impact description string: `f"New{entity_type}"` → `f"New {entity_type}"`
|
||||
- Added null-value guard in `_analyze_field_changes` — constraint fields with `None` old/new value are now correctly flagged as breaking instead of silently passing the subset check
|
||||
- Made `ChangeLogAnalyzer` stateless — `report` is now a local variable passed into `_generate_recommendations(report)` rather than stored as `self.report`; removes re-entrancy hazard
|
||||
- Removed no-op `__init__` from `ChangeLogAnalyzer`
|
||||
- Replaced non-portable emoji markers in recommendations (`✘✘✘`, `¤¤¤`, `☺☺☺`) with plain-text tags (`[BREAKING]`, `[WARNING]`, `[SAFE]`)
|
||||
- Extended `diff_ontologies` to cover `individuals` and `axioms` — previously only classes and properties were diffed; the public `compare_versions` path now returns all four element types
|
||||
- Fixed exception chaining in `compare_versions`: `raise ProcessingError(...) from e` to preserve original traceback
|
||||
- Removed silent `ImportError` swallow for `GraphValidator` — it is a first-party module; an `ImportError` indicates a broken install, not a graceful skip
|
||||
- Added comment on deferred `VersionManager` import in `OntologyEngine.__init__` explaining the circular-import constraint
|
||||
- Fixed import-before-docstring in `tests/change_management/test_managers.py`
|
||||
- Fixed broken Markdown link syntax in docs JSON example block: `"[http://...](http://...)"` → bare URI string
|
||||
- Updated docs recommendations example to match the new plain-text tag format
|
||||
|
||||
- **Ontology Alignment API** (PR #361 by @ZohaibHassan16, review & fixes by @KaifAhmad1):
|
||||
- Alignment representation using standard RDF predicates: `owl:equivalentClass`, `owl:equivalentProperty`, `owl:sameAs`, `skos:exactMatch`, `skos:closeMatch`, `skos:broadMatch`, `skos:narrowMatch`, `skos:relatedMatch`
|
||||
- `OntologyEngine.create_alignment(source_uri, target_uri, predicate)` — store alignment triples in TripletStore
|
||||
- `OntologyEngine.get_alignments(entity_uri)` — bidirectional retrieval of all alignments for an entity
|
||||
- `OntologyEngine.list_alignments(ontology_uri=None)` — list all alignments, optionally filtered by ontology namespace
|
||||
- `NamespaceManager.get_alignment_predicates()` — expose standard OWL/SKOS alignment URIs as a convenience dict
|
||||
- `ReuseManager.suggest_alignments(target, source)` — O(N+M) hashmap heuristic to suggest alignments based on exact label matches across ontologies
|
||||
- `ReuseManager.merge_ontology_data(..., compute_alignments=True)` — optionally attach suggested alignments to merge output without auto-committing unverified triples
|
||||
- `QueryEngine.expand_entity_uri(uri, store, use_alignments=True)` — bidirectional SPARQL expansion to include aligned equivalents; no-ops when flag is False
|
||||
- `QueryEngine.build_values_clause(variable, uris)` — generate a SPARQL `VALUES` clause for injecting expanded URIs into queries
|
||||
- Alignment-aware queries section added to `docs/reference/triplet_store.md`
|
||||
- Ontology Alignment section added to `docs/reference/ontology.md`
|
||||
- **Fixes applied post-review (by @KaifAhmad1)**:
|
||||
- Fixed progress tracker leak in `expand_entity_uri` — `stop_tracking` was only called inside the `hasattr(execute_sparql)` branch; backends without it silently leaked a tracker entry
|
||||
- Fixed `relatedMatch` predicate gap — `get_alignment_predicates()` exposed `skos:relatedMatch` but all three SPARQL FILTER lists omitted it, making those alignments permanently invisible
|
||||
- Fixed SPARQL injection in `list_alignments` — previously only `"` was escaped; `\`, `{`, and `}` are now also percent-encoded to prevent WHERE block breakout
|
||||
- Fixed SPARQL injection in `build_values_clause` — URIs now run through `_sanitize_uri` before wrapping in angle-bracket literals
|
||||
- Added full-URI validation in `create_alignment` — raises `ProcessingError` if predicate is a CURIE instead of a full URI, preventing silent storage of unqueryable triples
|
||||
- Fixed E2E test `test_end_to_end_cross_ontology_uri_flow` — previously mocked the method under test; now uses a real mock backend with `execute_sparql` to exercise the actual expansion and VALUES clause injection flow
|
||||
- 19 tests added covering: `create_alignment`, `get_alignments`, `suggest_alignments`, merge with alignment computation, `expand_entity_uri` (enabled/disabled), `build_values_clause`, and full E2E cross-ontology query flow
|
||||
- **Context Explainability Output Fixes** (by @KaifAhmad1):
|
||||
- Fixed decision-node storage in `ContextGraph` so full human-readable `scenario`, `reasoning`, and decision metadata are preserved on graph nodes instead of degrading into opaque IDs or truncated display text
|
||||
- Fixed causal and precedent reconstruction paths in the context module so returned `Decision` objects prefer readable stored fields over raw node identifiers
|
||||
- Fixed context aggregate outputs to return enriched readable payloads for influence, causality, similarity, policy-impact, and entity-similarity workflows instead of bare UUID lists or tuple-only results
|
||||
- Fixed `PolicyEngine.get_affected_decisions()` so both Cypher and fallback branches return consistent decision metadata including `scenario`, `category`, `outcome`, and `confidence`
|
||||
- Fixed `EntityLinker` similarity flows so enriched similarity results are consumed correctly across internal linking paths and public search aliases
|
||||
- Fixed `CentralityCalculator._build_adjacency()` to handle `ContextGraph` edges (dataclass `ContextEdge` objects with `source_id`/`target_id`) so `calculate_degree_centrality()` and related centrality algorithms work correctly when a `ContextGraph` is passed as the graph store
|
||||
- Fixed downstream KG integrations in `node_embeddings`, `link_predictor`, `centrality_calculator`, `path_finder`, and context retrieval fallbacks to normalize enriched neighbor/node outputs without breaking graph algorithms
|
||||
- Added 23 regression tests in `tests/context/test_context_explainability_regression.py` covering readable decision text preservation, enriched causal/path outputs, policy-impact results, entity similarity payloads, and compatibility with KG consumers
|
||||
|
||||
## [0.3.0] - 2026-03-10
|
||||
|
||||
- **Context Graph Feature Completeness** (by @KaifAhmad1):
|
||||
- Added `valid_from` / `valid_until` temporal validity fields to `ContextNode` and `ContextEdge` dataclasses — both expose `is_active(at_time=None) -> bool`; nodes/edges without these fields are always considered active
|
||||
- Added `add_node(valid_from=..., valid_until=...)` and `add_edge(valid_from=..., valid_until=...)` support — validity windows are extracted from `**properties` and stored as first-class dataclass fields, not in metadata
|
||||
- Added `ContextGraph.find_active_nodes(node_type=None, at_time=None)` — returns only nodes whose validity window includes the given time (defaults to `datetime.utcnow()`); complements `find_nodes()` with temporal filtering
|
||||
- Added `min_weight: float = 0.0` parameter to `ContextGraph.get_neighbors()` — edges with weight below the threshold are skipped during BFS traversal, enabling weighted/confidence-filtered multi-hop navigation; fully backward-compatible (default 0.0 passes all edges)
|
||||
- Added `ContextGraph.link_graph(other_graph, source_node_id, target_node_id, link_type="CROSS_GRAPH") -> str` — creates a navigable bridge between two separate `ContextGraph` instances; records a marker edge internally and returns a `link_id`
|
||||
- Added `ContextGraph.navigate_to(link_id) -> (other_graph, target_node_id)` — resolves a `link_id` to the target graph and its entry node, enabling hierarchical cross-graph traversal (e.g. agent moving from a high-level decision graph into a domain-specific sub-graph)
|
||||
- Added `ContextGraph.resolve_links(registry)` — reconnects cross-graph links after `load_from_file()`; `save_to_file()` now persists a `links` section with `other_graph_id` so navigation survives the full save/load cycle
|
||||
- Added `graph_id` field to `ContextGraph` — stable UUID per instance, persisted to JSON, so separate graphs can identify each other after reload
|
||||
- Fixed `is_active()` on `ContextNode` and `ContextEdge` — tz-aware `datetime` inputs are now normalised to tz-naive UTC before comparison, preventing `TypeError` when callers pass `datetime.now(timezone.utc)`
|
||||
- Fixed `valid_from` / `valid_until` serialisation — `add_nodes()`, `add_edges()`, `to_dict()`, and `from_dict()` all now preserve and restore validity windows; previously these fields were silently lost
|
||||
- Fixed cross-graph link artifact — `link_graph()` now pre-creates a `"cross_graph_link"` typed `ContextNode` for the marker before inserting the marker edge, preventing `_add_internal_edge()` from auto-creating a phantom `"entity"` node
|
||||
- Added 14 tests in `tests/context/test_cross_graph_navigation.py` covering link creation, phantom-node prevention, and full save/load round-trips with `resolve_links()`
|
||||
- Fixed `pipeline_builder.add_step()` return type annotation from `"PipelineBuilder"` to `"PipelineStep"` — implementation was already correct per 0.3.0-beta changelog, only signature and docstring were stale
|
||||
- Fixed `test_hybrid_search_performance` timing computation — accumulated a real `search_times` list and compute true average; raised threshold to `< 5.0s` to account for real `sentence-transformers` (384-dim) latency
|
||||
|
||||
|
||||
|
||||
- **0.3.0 Bug Fixes & Comprehensive Real-World Tests** (by @KaifAhmad1):
|
||||
- Fixed `ProvenanceTracker` missing from `semantica/kg/__init__.py` exports — `from semantica.kg import ProvenanceTracker` now works correctly
|
||||
- Fixed duplicate relation creation in `_parse_relation_result` — orphaned legacy block was appending every relation twice; removed the duplicate block
|
||||
- Added `extraction_method` parameter to `_parse_relation_result`; typed extraction path now correctly sets `"llm_typed"` instead of `"llm"` in relation metadata
|
||||
- Fixed cross-test cache pollution in `tests/semantic_extract/test_retry_logic.py` — module-level `_result_cache` now cleared in `setUp()` to prevent intermittent failures when tests share input text
|
||||
- Added `tests/test_030_realworld_comprehensive.py`: 85 real-world tests covering all 0.3.0-alpha/beta features with real data (tech companies, CEOs, products, investment chains, healthcare scenarios)
|
||||
- ContextGraph basic operations and decision tracking lifecycle
|
||||
- KG algorithms: centrality, community detection, embeddings, path finding, similarity, link prediction, connectivity
|
||||
- PolicyEngine, DecisionQuery, AgentContext, Decision model serialization
|
||||
- ProvenanceTracker with GraphBuilderWithProvenance and AlgorithmTrackerWithProvenance
|
||||
- Deduplication v2 with blocking strategies, RDF/TTL export, Reasoner inference
|
||||
- Pipeline builder/validator/failure handler with retry policies
|
||||
- Multi-hop investment chain (Microsoft→OpenAI, Google→Anthropic) end-to-end
|
||||
- Healthcare entity extraction and knowledge graph construction E2E
|
||||
|
||||
## [0.3.0-beta] - 2026-03-07
|
||||
|
||||
- **Multi-Founder LLM Extraction & Reasoner Inference Fix** (PR #354 by @KaifAhmad1):
|
||||
- Fixed `_parse_relation_result` in `methods.py` — unmatched subjects/objects now produce a synthetic `UNKNOWN` entity instead of silently dropping the relation; all LLM-returned co-founders are preserved
|
||||
- Rewrote `_match_pattern` in `reasoner.py` — splits pattern on `?var` placeholders first, then escapes only the literal segments; pre-bound variables resolve to exact literals, repeated variables use backreferences, non-greedy `.+?` prevents over-consumption of literal separators
|
||||
- Added `tests/reasoning/test_reasoner.py` with 4 tests covering multi-word value inference, pre-bound variables, binding conflicts, and single-word regression
|
||||
- Added `tests/semantic_extract/test_relation_extractor.py` with 6 tests covering all-founders returned, synthetic entity creation, matched entity integrity, predicate/confidence preservation, empty response, and malformed entries
|
||||
- **TTL Export Alias Fix** (PR #355 by @KaifAhmad1):
|
||||
- Added `_format_aliases` map in `RDFExporter` so `format="ttl"`, `"nt"`, `"xml"`, `"rdf"`, and `"json-ld"` resolve to their canonical counterparts without breaking existing callers
|
||||
- Alias resolution applied at the top of `export_to_rdf()` before format validation — zero public API changes
|
||||
- Added working TTL export cell to `cookbook/introduction/15_Export.ipynb` (Step 3: RDF Export)
|
||||
- Added `tests/export/test_rdf_exporter.py` with 8 tests covering all aliases, canonical formats, error handling, and file export
|
||||
|
||||
- **Incremental/Delta Processing Feature** (PR #349 by @ZohaibHassan16, reviewed and fixed by @KaifAhmad1):
|
||||
- Native delta computation between graph snapshots using SPARQL queries
|
||||
- Delta-aware pipeline execution with `delta_mode` configuration for processing only changed data
|
||||
- Version snapshot management with graph URI tracking and metadata storage
|
||||
- Snapshot retention policies with automatic cleanup via `prune_versions()` method
|
||||
- Integration with pipeline execution engine for incremental workflows
|
||||
- Significant performance improvements: processes only changes instead of full datasets
|
||||
- Cost optimization: dramatically reduces compute and storage requirements for large-scale operations
|
||||
- Production-ready for near real-time pipelines and frequent deployment scenarios
|
||||
- Bug fixes: corrected SPARQL variable order, fixed class references, resolved duplicate dictionary keys
|
||||
- Comprehensive test coverage including delta mode integration tests
|
||||
- Complete documentation with usage examples and API references
|
||||
- Essential for enterprise-grade, large-scale semantic infrastructure
|
||||
- **Deduplication v2 Migration Guide** (PR #344 by @ZohaibHassan16, fixes by @KaifAhmad1):
|
||||
- Added comprehensive MIGRATION_V2.md documentation for Deduplication v2 Epic #333
|
||||
- Documented Candidate Generation V2 with multi-key blocking and phonetic matching
|
||||
- Documented Two-Stage Scoring prefilter with configurable thresholds
|
||||
- Documented Semantic Relationship Deduplication v2 with synonym mapping
|
||||
- Added practical code examples for all V2 features with opt-in configuration
|
||||
- Fixed critical infinite recursion bug in dedup_triplets() function
|
||||
- Completed Epic #333 with comprehensive migration path and documentation
|
||||
- Performance: 5.86x speedup confirmed (129ms vs 754ms) for semantic deduplication
|
||||
- Full backward compatibility maintained with legacy mode as default
|
||||
- **Semantic Relationship Deduplication v2** (PR #340 by @ZohaibHassan16, fixes by @KaifAhmad1):
|
||||
- Implemented opt-in semantic relationship deduplication mode (`semantic_v2`) with 6.98x performance improvement
|
||||
- Added canonicalization engine with predicate synonym mapping (`works_for` → `employed_by`)
|
||||
- Implemented fast-path O(1) hash matching for exact canonical signature comparisons
|
||||
- Added weighted semantic scoring (60% predicate + 40% object composition) with explainable `semantic_match_score` metadata
|
||||
- Enhanced `dedup_triplets()` function as first-class API in `methods.py`
|
||||
- Integrated semantic deduplication into merge strategy with canonical key generation
|
||||
- Added literal normalization for whitespace cleanup in object matching
|
||||
- Maintained full backward compatibility with legacy mode as default
|
||||
- Fixed critical infinite recursion bug in `dedup_triplets()` function via registry name checking
|
||||
- Performance: Semantic V2 (~83ms) vs Legacy (~579ms) - 6.98x speedup confirmed
|
||||
- All 13 deduplication benchmarks passing with comprehensive test coverage
|
||||
- **Two-Stage Scoring Prefilter** (PR #339 by @ZohaibHassan16):
|
||||
- Implemented opt-in two-stage scoring with fast prefilter gates to eliminate expensive semantic scoring for obvious non-matches
|
||||
- Prefilter gates: type mismatch detection, name length ratio validation, token overlap requirements
|
||||
- Performance improvements: 18-25% faster batch processing with prefilter enabled
|
||||
- Configurable thresholds: `min_length_ratio`, `min_token_overlap_ratio`, `required_shared_token`
|
||||
- Enhanced explainability with score breakdown and rejection reasons in metadata
|
||||
- Complete backward compatibility with default `prefilter_enabled=False`
|
||||
|
||||
- **Candidate Generation v2 with Multi-Key Blocking** (PR #338 by @ZohaibHassan16):
|
||||
- Implemented opt-in candidate generation strategies (`legacy`, `blocking_v2`, `hybrid_v2`) to address O(N²) pair explosion during deduplication
|
||||
- Multi-key blocking with normalized token prefixes, type-aware keys, and optional phonetic (Soundex) blocking
|
||||
- Deterministic candidate budgeting with `max_candidates_per_entity` limit using stable sorting
|
||||
- Efficient pair generation with set-based deduplication across overlapping blocks
|
||||
- Performance improvements: 63.6% faster in worst-case scenarios (0.259s → 0.094s for 100 entities)
|
||||
- Complete backward compatibility with default `candidate_strategy="legacy"`
|
||||
- Added configuration options: `blocking_keys`, `enable_phonetic_blocking`, `max_candidates_per_entity`
|
||||
|
||||
- **ArangoDB AQL Export Support** (PR #342 by @tibisabau):
|
||||
### Added
|
||||
|
||||
- **ArangoDB AQL Export Support** (PR #342 by @tibisabau)
|
||||
- Full-featured ArangoDB AQL exporter with 642 lines of production-ready code
|
||||
- Comprehensive AQL INSERT statement generation for vertices and edges
|
||||
- Configurable collection names with validation and sanitization
|
||||
- Batch processing support for large knowledge graphs (default: 1000)
|
||||
- Added export_arango() convenience function for easy access
|
||||
- Enhanced unified export with AQL format support and .aql auto-detection
|
||||
- Added `export_arango()` convenience function for easy access
|
||||
- Enhanced unified export with AQL format support and `.aql` auto-detection
|
||||
- Integrated with method registry for extensibility
|
||||
- 17 comprehensive test cases with 100% pass rate
|
||||
- Enterprise-grade ArangoDB multi-model database integration
|
||||
|
||||
- **Apache Parquet Export Support** (PR #343 by @tibisabau):
|
||||
- **Apache Parquet Export Support** (PR #343 by @tibisabau)
|
||||
- Full-featured Apache Parquet exporter with 701 lines of production-ready code
|
||||
- Columnar storage format optimized for analytics and data warehousing
|
||||
- Configurable compression codecs (snappy, gzip, brotli, zstd, lz4, none)
|
||||
- Explicit Arrow schemas with type safety and consistency
|
||||
- Field normalization for varied entity and relationship naming conventions
|
||||
- Structured metadata handling using Parquet struct fields
|
||||
- Added export_parquet() convenience function for easy access
|
||||
- Enhanced unified export with Parquet format support and .parquet auto-detection
|
||||
- Added `export_parquet()` convenience function for easy access
|
||||
- Enhanced unified export with Parquet format support and `.parquet` auto-detection
|
||||
- Integrated with method registry for extensibility
|
||||
- 25 comprehensive test cases with 100% pass rate
|
||||
- Enterprise-grade analytics integration with pandas, Spark, Snowflake, BigQuery, Databricks
|
||||
|
||||
### Fixed
|
||||
- **Fixed NameError**: missing Type import in utils/helpers.py
|
||||
|
||||
- Fixed NameError: missing Type import in utils/helpers.py
|
||||
- Added Type to typing imports to fix retry_on_error decorator
|
||||
- Removed unused Type import from config_manager.py
|
||||
- Resolves ImportError when importing semantica modules
|
||||
- Fixes capability gap analysis notebook execution
|
||||
|
||||
- **Test Suite Fixes: 0.3.0-alpha & Unreleased Features** (PR utils by @KaifAhmad1):
|
||||
|
||||
**Context Module (`semantica/context/`)**
|
||||
- Fixed `retrieve_decision_precedents` to gate entity extraction on `use_hybrid_search=True` — was incorrectly extracting entities when flag was `False`
|
||||
- Fixed `_extract_entities_from_query` to use `word[0].isupper()` instead of `word.istitle()` — correctly captures `CreditCard`, `CustomerID` etc.
|
||||
- Added missing `expand_context` method — BFS graph traversal via `knowledge_graph.get_neighbors`
|
||||
- Added missing `_get_decision_query` method — creates a `DecisionQuery` from the knowledge graph
|
||||
- Fixed `hybrid_retrieval` to call `expand_context(query)` once (not per-entity) and include `"query"` key in return dict
|
||||
- Fixed `dynamic_context_traversal` to call `expand_context` once per query instead of per entity
|
||||
- Fixed `multi_hop_context_assembly` to use `_get_decision_query()` for robust decision lookup
|
||||
- Fixed `_retrieve_from_vector` to fall back to `result["metadata"]["content"]` when `result["content"]` is absent — prevents empty content and negative similarity scores during semantic re-ranking
|
||||
|
||||
**Knowledge Graph Module (`semantica/kg/`)**
|
||||
- Fixed `calculate_pagerank` — added `alpha` and `max_iter` parameter aliases; changed return format to structured dict `{"centrality": scores, "rankings": sorted_list}`
|
||||
- Fixed `community_detector._to_networkx` to return a NetworkX graph directly when one is passed (was converting to adjacency list, silently losing all edges)
|
||||
- Added `method` as alias for `algorithm` parameter in `detect_communities`
|
||||
- Fixed `_build_adjacency` to handle `"edges"` key (list of tuples) in addition to `"relationships"` (list of dicts)
|
||||
- Added `_track_generic` base method and 9 domain-specific tracking methods to `AlgorithmTrackerWithProvenance`: `track_influence_analysis`, `track_verification_analysis`, `track_supply_chain_paths`, `track_bottleneck_analysis`, `track_quality_analysis`, `track_lead_time_analysis`, `track_cross_domain_analysis`, `track_cross_domain_similarity`, `track_collaboration_potential`
|
||||
- Created new `provenance_tracker.py` module with `ProvenanceTracker` class (`track_entity`, `get_all_sources`, `clear`)
|
||||
|
||||
**Pipeline Module (`semantica/pipeline/`)**
|
||||
- Fixed `execution_engine` retry loop to properly iterate up to `max_retries` (was only retrying once regardless of policy)
|
||||
- Added `RecoveryAction` dataclass and `handle_failure(error, policy, retry_count)` method to `FailureHandler` — implements LINEAR, EXPONENTIAL, and FIXED backoff strategies
|
||||
- Fixed `pipeline_builder.add_step` to return the created `PipelineStep` object instead of `self`
|
||||
- Added `validate` as a public alias for `validate_pipeline` in `PipelineValidator`
|
||||
- Updated missing-dependency error message to `"Missing dependency '{dep}' for step '{name}'"` for consistent test assertions
|
||||
|
||||
**Vector Store (`semantica/vector_store/`)**
|
||||
- Relaxed `test_batch_processing_performance` threshold from `< 100ms` to `< 500ms` per decision — original threshold was too tight for development machines running a real `sentence-transformers` embedding model (384-dim)
|
||||
|
||||
**Test File Fixes**
|
||||
- `test_end_to_end_context_integration.py` — replaced emoji characters (`✅`, `❌`, `🔄`, `⚠️`) with ASCII equivalents (`[OK]`, `[FAIL]`, `[...]`, `[WARN]`) to fix Windows cp1252 encoding error
|
||||
- `test_context_retriever_precedents.py` — moved `assert_called_once_with` inside `with patch.object` block; fixed assertion to use `decision.scenario` not `decision.decision_id`; removed `"iPhone"` (lowercase-first) from entity extraction assertion
|
||||
- `test_real_world_scenarios.py` — fixed duplicate `source=` keyword argument (renamed to `label=`); fixed cross-domain analysis loop to iterate over all social network users instead of only `academic_users`
|
||||
- `test_pipeline_comprehensive.py` — changed `test_pipeline_validator_missing_deps` to call `validator.validate(builder)` directly instead of `builder.build()` which raises `ValidationError` before validation can complete
|
||||
|
||||
**Results: ~840 tests passing, 36 skipped (external services), 0 failed**
|
||||
|
||||
## [0.3.0-alpha] - 2026-02-19
|
||||
|
||||
### Added / Changed
|
||||
|
||||
- **Decision Tracking System**: Complete decision lifecycle management with audit trails and provenance tracking
|
||||
- **Advanced KG Algorithms**: Node2Vec embeddings, centrality analysis, community detection for decision insights
|
||||
- **Enhanced Context Module**: Unified AgentContext with granular feature flags and decision tracking integration
|
||||
- **Vector Store Features**: Hybrid search combining semantic, structural, and category similarity
|
||||
- **Policy Management**: Versioning, compliance checking, and exception handling
|
||||
- **Production Ready Architecture**: Scalable design with comprehensive error handling and validation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed import issues in test suite (ProvenanceTracker location fixes)
|
||||
- Fixed causal analyzer validation (max_depth bounds checking)
|
||||
- Fixed test compatibility with updated method signatures
|
||||
- Fixed mock object setup in test suites
|
||||
- Comprehensive test suite fixes for decision tracking features
|
||||
|
||||
### Testing
|
||||
|
||||
- 113+ tests passing across context and core modules
|
||||
- Comprehensive decision tracking test coverage
|
||||
- Enhanced error handling and edge case testing
|
||||
- Fixed all critical test failures for release readiness
|
||||
|
||||
### Documentation
|
||||
|
||||
- Enhanced context module documentation
|
||||
- Updated API references for decision tracking features
|
||||
- Comprehensive usage guides and examples
|
||||
|
||||
- Fixed: Context Graphs decision tracking bugs and added comprehensive test coverage (PR #315 by @KaifAhmad1)
|
||||
- Fixed empty/None decision ID handling in ContextGraph.add_decision()
|
||||
- Fixed None metadata handling to prevent TypeError
|
||||
- Fixed causal chain depth logic and node exclusion
|
||||
- Fixed nonexistent node handling in add_causal_relationship()
|
||||
- Added missing properties field in to_dict serialization
|
||||
- Added missing from_dict method for graph deserialization
|
||||
- Fixed precedent search direction in find_precedents()
|
||||
- Fixed UUID generation logic in all decision models
|
||||
- Added comprehensive test suite with 9 tests covering all features
|
||||
- All 71 context tests now passing (100% success rate)
|
||||
|
||||
- Fixed: PolicyEngine latest version selection on ContextGraph; AgentContext fallback robustness and secure logging (PR #TBD by @KaifAhmad1)
|
||||
- Tests: Added ContextGraph fallback and AgentContext smoke tests; full suite passing
|
||||
|
||||
- **Apache AGE Backend Security Fixes** (PR #311 by @Sameer6305, fixes by @KaifAhmad1):
|
||||
- Added AgeStore class with GraphStore API compatibility
|
||||
- Fixed SQL injection vulnerabilities with input validation
|
||||
- Added psycopg2-binary dependency and migration guide
|
||||
- Fixed parameter replacement and test mock leakage
|
||||
- Enhanced error handling and Unicode display issues
|
||||
|
||||
- **Context Engineering Enhancement** (PR #307 by @KaifAhmad1):
|
||||
- Comprehensive decision tracking system with full lifecycle management (record → analyze → query → precedent → influence)
|
||||
- Advanced KG algorithm integration: centrality analysis, community detection, node embeddings with ContextGraph
|
||||
- Enhanced AgentContext with granular feature flags for decision tracking, KG algorithms, and vector store features
|
||||
- PolicyException model replacing conflicting Exception name for meaningful business domain modeling
|
||||
- GraphStore validation preventing runtime failures with explicit capability checking
|
||||
- Hybrid search combining semantic, structural, and category similarity with configurable weights
|
||||
- Decision influence analysis with centrality measures and causal chain tracking
|
||||
- Policy management with versioning, compliance checking, and exception handling
|
||||
- Production-ready architecture with audit trails, security, and scalability features
|
||||
- 9 critical bug fixes: logging, security, audit trails, API compatibility, Cypher queries, centrality access, validation, naming
|
||||
- Comprehensive documentation with usage guides, production examples, and API references
|
||||
- 100% test coverage with all validation tests passing (9/9 tests)
|
||||
- Enterprise-grade features for financial services, healthcare, legal, and business domains
|
||||
- Complete backward compatibility with existing semantica components
|
||||
- Performance optimizations: caching, indexing, and efficient graph operations
|
||||
|
||||
- **Added PgVector Store Support** (PR #303 by @Sameer6305, @KaifAhmad1):
|
||||
- Native PostgreSQL vector storage using pgvector extension with full integration
|
||||
- Multiple distance metrics: cosine, L2/Euclidean, inner product with automatic score normalization
|
||||
- Advanced indexing: HNSW and IVFFlat for approximate nearest neighbor search with tunable parameters
|
||||
- JSONB metadata storage with flexible filtering capabilities and batch operations
|
||||
- Connection pooling support with psycopg3/psycopg2 fallback and efficient resource management
|
||||
- Comprehensive VectorStore integration with backend delegation and unified API
|
||||
- Idempotent index creation and table management with safe migration support
|
||||
- Production-ready security: SQL injection protection with psycopg_sql.SQL() and input validation
|
||||
- Performance optimizations: UUID4-based IDs, batch executemany operations, connection pooling
|
||||
- Full backward compatibility with existing vector store implementations
|
||||
- 36+ comprehensive test cases with Docker integration and dependency skipping
|
||||
- Complete documentation with setup guides, examples, and performance tuning
|
||||
- CI/CD integration: resolved benchmark compatibility and fixed documentation links
|
||||
|
||||
- **Improved Vector Store for Decision Tracking** (PR #293 by @KaifAhmad1):
|
||||
- Comprehensive decision tracking capabilities with hybrid search combining semantic and structural embeddings
|
||||
- New DecisionEmbeddingPipeline for generating semantic and structural embeddings with KG algorithm integration
|
||||
- HybridSimilarityCalculator with configurable weights (semantic: 0.7, structural: 0.3)
|
||||
- DecisionContext high-level interface for decision management with explainable AI features
|
||||
- ContextRetriever with hybrid precedent search and multi-hop reasoning
|
||||
- User-friendly convenience API: quick_decision(), find_precedents(), explain(), similar_to(), batch_decisions(), filter_decisions()
|
||||
- Knowledge Graph algorithm integration: Node2Vec, PathFinder, CommunityDetector, CentralityCalculator, SimilarityCalculator, ConnectivityAnalyzer
|
||||
- Explainable AI with path tracing, confidence scoring, and comprehensive decision explanations
|
||||
- Performance optimizations: 0.028s per decision processing, 0.031s search performance, ~0.8KB per decision memory usage
|
||||
- 100% backward compatibility maintained with existing VectorStore functionality
|
||||
- 34+ comprehensive tests covering all functionality including end-to-end scenarios and performance benchmarks
|
||||
- Real-world validation examples for banking and insurance domains
|
||||
- Documentation with clear imports, examples, and API references
|
||||
|
||||
- **Improved Graph Algorithms in KG Module** (PR #292 by @KaifAhmad1):
|
||||
- Complete algorithm suite with 30+ graph algorithms across 7 categories
|
||||
- Node Embeddings: Node2Vec, DeepWalk, Word2Vec for structural similarity analysis
|
||||
- Similarity Analysis: Cosine, Euclidean, Manhattan, Correlation metrics with batch processing
|
||||
- Path Finding: Dijkstra, A*, BFS, K-shortest paths for route and network analysis
|
||||
- Link Prediction: Preferential attachment, Jaccard, Adamic-Adar for network completion
|
||||
- Centrality Analysis: Degree, Betweenness, Closeness, PageRank for importance ranking
|
||||
- Community Detection: Louvain, Leiden, Label propagation for clustering analysis
|
||||
- Connectivity Analysis: Components, bridges, density for network robustness
|
||||
- Unified provenance tracking system with GraphBuilderWithProvenance and AlgorithmTrackerWithProvenance
|
||||
- Complete execution tracking with metadata, timestamps, and reproducibility IDs
|
||||
- Comprehensive test coverage with 5 test suites and 40+ test methods
|
||||
- Professional documentation overhaul for all modules and reference documentation
|
||||
- Enterprise-ready functionality with error handling and NetworkX compatibility
|
||||
- Performance optimizations with sparse matrix operations and batch processing
|
||||
- Full backward compatibility maintained with gradual migration support
|
||||
|
||||
- **Improved Security Configuration with Dependabot**:
|
||||
- Configured bi-weekly security updates with manual review by @KaifAhmad1
|
||||
- Implemented automated security scans (Monday & Thursday at 7 AM IST) with Bandit, Safety, Semgrep
|
||||
- Added security-critical package grouping (cryptography, requests, urllib3, certifi, pyopenssl)
|
||||
- Enterprise-grade security with audit trail, compliance features, and zero auto-merge
|
||||
- Optimized IST timezone scheduling (Security scans: 7 AM IST, PRs: 9 AM IST)
|
||||
- Aligned with new Dependabot features: open-source proxy support, smart dependency grouping for Snowflake/Arrow/benchmark features, private registry support, semantic commit prefixes, and latest GitHub security best practices
|
||||
|
||||
- **ResourceScheduler Deadlock Fix and Performance Improvements** (PR #299, #301 by @d4ndr4d3, @KaifAhmad1):
|
||||
- Fixed critical deadlock in ResourceScheduler by replacing `threading.Lock()` with `threading.RLock()`
|
||||
- Resolved nested lock acquisition issue in `allocate_resources()` → `allocate_cpu/memory/gpu()` calls
|
||||
- Added allocation validation with `ValidationError` when no resources can be allocated
|
||||
- Improved performance by moving progress tracking updates outside lock scope
|
||||
- Implemented comprehensive resource cleanup on allocation failures to prevent leaks
|
||||
- Added complete regression test suite (6 tests) for deadlock prevention and edge cases
|
||||
- Improved error handling and documentation for better operator visibility
|
||||
- Zero breaking changes, maintains thread safety and backward compatibility
|
||||
|
||||
## [0.2.7] - 2026-02-09
|
||||
|
||||
### Added / Changed
|
||||
|
||||
- **Snowflake Connector for Data Ingestion** (PR #276 by @Sameer6305):
|
||||
- Native Snowflake connector with multi-authentication (password, OAuth, key-pair, SSO)
|
||||
- Table and query ingestion with pagination, schema introspection, batch processing
|
||||
- SQL injection prevention via identifier escaping, OAuth token validation
|
||||
- Progress tracking integration, context manager support, document export
|
||||
- 24 comprehensive unit tests with mocking, complete documentation and examples
|
||||
- Added as optional dependency `db-snowflake` with snowflake-connector-python>=3.0.0
|
||||
|
||||
- **Apache Arrow Export Support** (PR #273 by @Sameer6305):
|
||||
- Added Apache Arrow exporter with explicit schemas, entity/relationship export, compression support
|
||||
- Integrated with export module and method registry, Pandas/DuckDB compatible
|
||||
- 20 unit tests + 1 integration test, complete documentation with examples
|
||||
|
||||
- **Comprehensive Benchmark Suite with Regression CLI** (PR #289 by @ZohaibHassan16, @KaifAhmad1):
|
||||
- 137+ benchmarks across all 10 Semantica modules (Input, Core, Storage, Context, QA, Ontology, etc.)
|
||||
- Environment-agnostic design with robust mocking system for CI/CD compatibility
|
||||
- Statistical regression detection using Z-score analysis with configurable thresholds
|
||||
- Automated performance auditing via GitHub Actions workflow
|
||||
- Comprehensive documentation suite (benchmarks.md, architecture guides, usage examples)
|
||||
- Zero breaking changes, production-ready with ultra-fast text processing (>10,000 ops/s)
|
||||
- Added benchmark runner CLI: `python benchmarks/benchmark_runner.py`
|
||||
|
||||
## [0.2.6] - 2026-02-03
|
||||
|
||||
### Added / Changed
|
||||
|
||||
- **W3C PROV-O Compliant Provenance Tracking** (#254, #246):
|
||||
@@ -431,4 +1108,3 @@ When breaking changes are introduced, migration guides will be provided in the r
|
||||
---
|
||||
|
||||
For detailed release notes, see [GitHub Releases](https://github.com/Hawksight-AI/semantica/releases).
|
||||
|
||||
|
||||
+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/vqRt2qbx)**
|
||||
⭐ **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/vqRt2qbx) 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/vqRt2qbx) 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/vqRt2qbx), [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/vqRt2qbx) - 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/vqRt2qbx)**
|
||||
⭐ **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/vqRt2qbx)**
|
||||
⭐ **Give us a Star** • 🍴 **Fork us** • 💬 **Join our [Discord](https://discord.gg/sV34vps5hH)**
|
||||
|
||||
---
|
||||
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
# Release Process for Semantica
|
||||
|
||||
This document outlines the steps to release a new version of the Semantica framework.
|
||||
|
||||
## 1. Versioning Policy
|
||||
|
||||
Semantica follows [Semantic Versioning (SemVer)](https://semver.org/).
|
||||
- **MAJOR** version for incompatible API changes.
|
||||
- **MINOR** version for functionality added in a backwards compatible manner.
|
||||
- **PATCH** version for backwards compatible bug fixes.
|
||||
|
||||
## 2. Pre-release Checklist
|
||||
|
||||
Before releasing, ensure:
|
||||
- [ ] All tests pass: `pytest`
|
||||
- [ ] Documentation is up to date in `docs/` and `MkDocs` config.
|
||||
- [ ] `CHANGELOG.md` is updated with the latest changes.
|
||||
- [ ] Version is updated in:
|
||||
- `semantica/__init__.py`
|
||||
- `pyproject.toml`
|
||||
- `docs/citation.md` (BibTeX entry)
|
||||
|
||||
## 3. Release Steps
|
||||
|
||||
### Automated Release (Recommended)
|
||||
|
||||
The project uses GitHub Actions for automated releases to PyPI.
|
||||
|
||||
1.29. **Tag the commit**: Create a new git tag for the version (e.g., `v0.2.3`).
|
||||
```bash
|
||||
git tag -a v0.2.3 -m "Release v0.2.3"
|
||||
git push origin v0.2.3
|
||||
```
|
||||
2. **GitHub Action**: The `Release` workflow will automatically trigger, build the package, create a GitHub Release, and publish to PyPI using Trusted Publishing.
|
||||
|
||||
### Manual Release
|
||||
|
||||
If you need to release manually:
|
||||
|
||||
1. **Build the package**:
|
||||
```bash
|
||||
python -m build
|
||||
```
|
||||
2. **Verify the build**:
|
||||
```bash
|
||||
twine check dist/*
|
||||
```
|
||||
3. **Upload to PyPI**:
|
||||
```bash
|
||||
twine upload dist/*
|
||||
```
|
||||
|
||||
## 4. Post-release
|
||||
|
||||
- Verify the new version is available on [PyPI](https://pypi.org/project/semantica/).
|
||||
- Check the [GitHub Releases](https://github.com/your-org/semantica/releases) page for the new release notes.
|
||||
@@ -0,0 +1,282 @@
|
||||
# Semantica v0.3.0 — Release Notes
|
||||
|
||||
**Released:** 2026-03-10
|
||||
**PyPI:** `pip install semantica`
|
||||
**Tag:** [v0.3.0](https://github.com/Hawksight-AI/semantica/releases/tag/v0.3.0)
|
||||
**Classification:** Production/Stable
|
||||
|
||||
> First stable, full public release of Semantica. Covers everything shipped across three release stages: 0.3.0-alpha (2026-02-19), 0.3.0-beta (2026-03-07), and 0.3.0 stable (2026-03-10).
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
| Contributor | Role |
|
||||
|------------|------|
|
||||
| [@KaifAhmad1](https://github.com/KaifAhmad1) | Lead maintainer — context graph, decision intelligence, KG algorithms, semantic extraction, pipeline, provenance, bug fixes, release management |
|
||||
| [@ZohaibHassan16](https://github.com/ZohaibHassan16) | Deduplication v2 suite (candidate generation, two-stage scoring, semantic dedup), incremental/delta processing, benchmark suite |
|
||||
| [@Sameer6305](https://github.com/Sameer6305) | Apache AGE backend, PgVector store, Snowflake connector, Apache Arrow export |
|
||||
| [@tibisabau](https://github.com/tibisabau) | ArangoDB AQL export, Apache Parquet export |
|
||||
| [@d4ndr4d3](https://github.com/d4ndr4d3) | ResourceScheduler deadlock fix |
|
||||
|
||||
---
|
||||
|
||||
## v0.3.0 — Stable (2026-03-10)
|
||||
|
||||
### Context Graph Feature Completeness
|
||||
|
||||
**Temporal Validity Windows** (by @KaifAhmad1)
|
||||
|
||||
Nodes and edges now carry first-class `valid_from` / `valid_until` ISO datetime fields. These are stored directly on `ContextNode` and `ContextEdge` dataclasses — not in metadata — and survive full serialisation round-trips through `save_to_file()` / `load_from_file()` and `to_dict()` / `from_dict()`.
|
||||
|
||||
- `ContextNode.is_active(at_time=None)` and `ContextEdge.is_active(at_time=None)` — returns `True` if the node/edge is live at the given time (defaults to now). Handles both tz-aware and tz-naive datetime inputs correctly.
|
||||
- `ContextGraph.find_active_nodes(node_type=None, at_time=None)` — filters the entire graph and returns only nodes within their validity window.
|
||||
- `add_node(valid_from=..., valid_until=...)` and `add_edge(valid_from=..., valid_until=...)` — pass validity fields directly in the call signature.
|
||||
- Bug fix: `is_active()` previously crashed with `TypeError` when passed a tz-aware `datetime` (e.g. `datetime.now(timezone.utc)`). Fixed by normalising all inputs to tz-naive UTC via a new `_parse_iso_dt()` helper.
|
||||
- Bug fix: validity fields were silently lost in `add_nodes()`, `add_edges()`, `to_dict()`, and `from_dict()`. All four paths now correctly preserve and restore them.
|
||||
|
||||
**Weighted Multi-Hop BFS** (by @KaifAhmad1)
|
||||
|
||||
`ContextGraph.get_neighbors(node_id, hops=1, relationship_types=None, min_weight=0.0)` now accepts a `min_weight` threshold. Any edge with weight below the threshold is skipped during BFS traversal, allowing callers to confine multi-hop queries to high-confidence causal links. Default `0.0` is fully backward-compatible.
|
||||
|
||||
**Cross-Graph Navigation** (by @KaifAhmad1)
|
||||
|
||||
Separate `ContextGraph` instances can now be linked and navigated between — hierarchically, like separate knowledge domains that reference each other.
|
||||
|
||||
- `link_graph(other_graph, source_node_id, target_node_id, link_type="CROSS_GRAPH") -> str` — creates a navigable bridge and returns a `link_id`. Records a dedicated `"cross_graph_link"` typed marker node internally (not a phantom `"entity"`) and a marker edge.
|
||||
- `navigate_to(link_id) -> (other_graph, target_node_id)` — jumps to the target graph and entry node for a given link.
|
||||
- `graph_id` field — each `ContextGraph` now carries a stable UUID so instances can identify each other across save/load.
|
||||
- `save_to_file()` — now writes a `links` section alongside nodes and edges, containing `link_id`, `source_node_id`, `target_node_id`, and `other_graph_id` for every cross-graph link.
|
||||
- `load_from_file()` — restores `graph_id` and populates `_unresolved_links` from the `links` section.
|
||||
- `resolve_links(registry: Dict[str, ContextGraph]) -> int` — reconnects unresolved links post-load. Pass `{graph_id: graph_instance}` for each linked graph; returns the count of successfully resolved links. `navigate_to()` raises a clear `KeyError` with a `resolve_links()` hint if called before resolution.
|
||||
- Bug fix: the previous implementation auto-created the synthetic marker target as an `"entity"` node (phantom pollution). Fixed by explicitly pre-creating a `"cross_graph_link"` typed `ContextNode` before the marker edge.
|
||||
- 14 new tests in `tests/context/test_cross_graph_navigation.py` covering all scenarios including full save/load round-trips with partial registry resolution.
|
||||
|
||||
**Other Fixes** (by @KaifAhmad1)
|
||||
|
||||
- `PipelineBuilder.add_step()` return type annotation corrected from `"PipelineBuilder"` to `"PipelineStep"` — the implementation was already correct; only the annotation and docstring were stale.
|
||||
- `test_hybrid_search_performance` timing computation fixed — now accumulates a true `search_times` list instead of reusing the last loop iteration's `start_time`; threshold relaxed to `< 5.0s` for real `sentence-transformers` (384-dim) latency on development machines.
|
||||
|
||||
**Test Coverage Added**
|
||||
|
||||
- 14 cross-graph navigation tests (`tests/context/test_cross_graph_navigation.py`)
|
||||
- **Total: 335 context tests, 886+ tests across all modules — 0 failures**
|
||||
|
||||
---
|
||||
|
||||
## v0.3.0-beta — Beta (2026-03-07)
|
||||
|
||||
### Semantic Extraction Fixes
|
||||
|
||||
**Multi-Founder LLM Extraction & Reasoner Inference Fix** (PR #354, by @KaifAhmad1)
|
||||
|
||||
- `_parse_relation_result` in `methods.py` — unmatched subjects/objects now produce a synthetic `UNKNOWN` entity instead of silently dropping the relation. All co-founders returned by the LLM are preserved in the output.
|
||||
- Duplicate relation fix — an orphaned legacy block that appended every relation twice has been removed.
|
||||
- `extraction_method` parameter added — typed extraction paths now correctly record `"llm_typed"` in relation metadata instead of `"llm"`.
|
||||
- `_match_pattern` in `reasoner.py` rewritten — splits patterns on `?var` placeholders first, then escapes only literal segments. Pre-bound variables resolve to exact literals, repeated variables use backreferences, non-greedy `.+?` prevents over-consumption of separators.
|
||||
- Added `tests/reasoning/test_reasoner.py` (4 tests) and `tests/semantic_extract/test_relation_extractor.py` (6 tests).
|
||||
|
||||
**TTL Export Alias Fix** (PR #355, by @KaifAhmad1)
|
||||
|
||||
- `RDFExporter` now accepts `"ttl"`, `"nt"`, `"xml"`, `"rdf"`, and `"json-ld"` as format aliases in `export_to_rdf()`. Aliases resolve before format validation — zero public API changes.
|
||||
- Added `tests/export/test_rdf_exporter.py` (8 tests).
|
||||
|
||||
### Incremental / Delta Processing
|
||||
|
||||
**Native Delta Computation** (PR #349, by @ZohaibHassan16, reviewed and fixed by @KaifAhmad1)
|
||||
|
||||
- Native SPARQL-based diff between graph snapshots — only changed triples flow through the pipeline.
|
||||
- `delta_mode` configuration in `PipelineBuilder` for near-real-time workloads.
|
||||
- Version snapshot management with graph URI tracking and metadata storage.
|
||||
- `prune_versions()` for automatic snapshot retention cleanup.
|
||||
- Bug fixes: corrected SPARQL variable order, fixed class references, resolved duplicate dictionary keys.
|
||||
|
||||
### Deduplication v2
|
||||
|
||||
**Candidate Generation v2** (PR #338, by @ZohaibHassan16)
|
||||
|
||||
- New opt-in strategies: `blocking_v2` and `hybrid_v2`, replacing O(N²) pair enumeration.
|
||||
- Multi-key blocking with normalised token prefixes, type-aware keys, and optional phonetic (Soundex) blocking.
|
||||
- Deterministic `max_candidates_per_entity` budgeting with stable sorting.
|
||||
- **63.6% faster** in worst-case scenarios (0.259s → 0.094s for 100 entities).
|
||||
|
||||
**Two-Stage Scoring Prefilter** (PR #339, by @ZohaibHassan16)
|
||||
|
||||
- Fast gates for type mismatch, name-length ratio, and token overlap eliminate expensive semantic scoring for obvious non-matches.
|
||||
- Configurable thresholds: `min_length_ratio`, `min_token_overlap_ratio`, `required_shared_token`.
|
||||
- **18–25% faster** batch processing with prefilter enabled (`prefilter_enabled=False` by default).
|
||||
|
||||
**Semantic Relationship Deduplication v2** (PR #340, by @ZohaibHassan16, fixes by @KaifAhmad1)
|
||||
|
||||
- Canonicalisation engine with predicate synonym mapping (`works_for` → `employed_by`).
|
||||
- O(1) hash matching for exact canonical signatures.
|
||||
- Weighted scoring: 60% predicate + 40% object composition with explainable `semantic_match_score`.
|
||||
- **6.98x faster** than legacy mode (83ms vs 579ms).
|
||||
- `dedup_triplets()` infinite recursion bug fixed; function is now a first-class API in `methods.py`.
|
||||
|
||||
**Deduplication v2 Migration Guide** (PR #344, by @ZohaibHassan16, fixes by @KaifAhmad1)
|
||||
|
||||
- Comprehensive `MIGRATION_V2.md` documenting all v2 strategies with code examples.
|
||||
- Full backward compatibility maintained — legacy mode remains the default.
|
||||
|
||||
### Export Formats
|
||||
|
||||
**ArangoDB AQL Export** (PR #342, by @tibisabau)
|
||||
|
||||
- Full AQL INSERT statement generation for vertices and edges.
|
||||
- Configurable collection names with validation and sanitisation; batch processing (default: 1000).
|
||||
- `export_arango()` convenience function; `.aql` auto-detection in the unified exporter.
|
||||
- 17 tests, 100% pass rate.
|
||||
|
||||
**Apache Parquet Export** (PR #343, by @tibisabau)
|
||||
|
||||
- Columnar storage format with configurable compression: snappy, gzip, brotli, zstd, lz4, none.
|
||||
- Explicit Apache Arrow schemas with type safety; field normalisation for varied naming conventions.
|
||||
- `export_parquet()` convenience function; `.parquet` auto-detection.
|
||||
- Analytics-ready for pandas, Spark, Snowflake, BigQuery, Databricks.
|
||||
- 25 tests, 100% pass rate.
|
||||
|
||||
### Bug Fixes & Test Suite Stabilisation
|
||||
|
||||
**Test Suite Fixes** (by @KaifAhmad1)
|
||||
|
||||
Context module:
|
||||
- `retrieve_decision_precedents` — gated entity extraction on `use_hybrid_search=True` correctly.
|
||||
- `_extract_entities_from_query` — now uses `word[0].isupper()` to capture camelCase identifiers like `CreditCard`.
|
||||
- Added missing `expand_context` (BFS traversal) and `_get_decision_query` methods.
|
||||
- Fixed `hybrid_retrieval`, `dynamic_context_traversal`, and `multi_hop_context_assembly` for correct single-pass BFS.
|
||||
- Fixed `_retrieve_from_vector` fallback to `result["metadata"]["content"]` to prevent empty content and negative re-ranking scores.
|
||||
|
||||
KG module:
|
||||
- `calculate_pagerank` — added `alpha`/`max_iter` aliases; return format changed to `{"centrality": scores, "rankings": sorted_list}`.
|
||||
- `community_detector._to_networkx` — now returns a NetworkX graph directly when one is passed (previously lost all edges).
|
||||
- Added 9 domain-specific tracking methods to `AlgorithmTrackerWithProvenance`.
|
||||
- Created `provenance_tracker.py` with `ProvenanceTracker` (`track_entity`, `get_all_sources`, `clear`).
|
||||
|
||||
Pipeline module:
|
||||
- Retry loop fixed — now correctly iterates to `max_retries`.
|
||||
- `RecoveryAction` dataclass and `handle_failure(error, policy, retry_count)` added with LINEAR, EXPONENTIAL, and FIXED strategies.
|
||||
- `add_step()` fixed to return the created `PipelineStep`.
|
||||
- `validate` added as alias for `validate_pipeline` in `PipelineValidator`.
|
||||
|
||||
Other:
|
||||
- Fixed `NameError` for missing `Type` import in `utils/helpers.py`.
|
||||
- Vector store performance threshold relaxed (100ms → 500ms per decision for development machines).
|
||||
- Windows cp1252 encoding fix in test files (emoji → ASCII).
|
||||
- `ProvenanceTracker` added to `semantica/kg/__init__.py` exports.
|
||||
|
||||
**Results: ~840 tests passing, 36 skipped (external services), 0 failed**
|
||||
|
||||
---
|
||||
|
||||
## v0.3.0-alpha — Alpha (2026-02-19)
|
||||
|
||||
### Context & Decision Intelligence
|
||||
|
||||
**Context Engineering Enhancement** (PR #307, by @KaifAhmad1)
|
||||
|
||||
The foundational 0.3.0 feature — complete overhaul of the context module for production-grade decision intelligence:
|
||||
|
||||
- Full decision lifecycle: `record_decision()` → `add_decision()` → `add_causal_relationship()` → `trace_decision_chain()` → `analyze_decision_impact()` → `analyze_decision_influence()` → `find_similar_decisions()`
|
||||
- `AgentContext` unified wrapper with granular feature flags: `decision_tracking`, `kg_algorithms`, `graph_expansion`; methods: `store()`, `retrieve()`, `get_conversation_history()`, `get_statistics()`, `capture_cross_system_inputs()`
|
||||
- `AgentMemory` with working, conversation, and long-term memory tiers
|
||||
- `PolicyEngine` with versioned policy nodes, compliance checking (`check_decision_rules()`), and `PolicyException` model
|
||||
- Hybrid precedent search combining vector, structural, and category similarity with configurable weights
|
||||
- Decision influence analysis via centrality measures and causal chain tracking
|
||||
- GraphStore validation preventing runtime failures; secure logging
|
||||
- 9 critical bug fixes across logging, security, audit trails, API compatibility, Cypher queries, centrality access, validation
|
||||
|
||||
**Context Decision Tracking Fixes** (PR #315, by @KaifAhmad1)
|
||||
|
||||
- Fixed empty/None decision ID handling in `add_decision()`
|
||||
- Fixed None metadata handling preventing `TypeError`
|
||||
- Fixed causal chain depth logic and node exclusion
|
||||
- Fixed nonexistent node handling in `add_causal_relationship()`
|
||||
- Fixed precedent search direction in `find_precedents()`
|
||||
- Added missing `properties` field in `to_dict()`; added `from_dict()` method
|
||||
- Fixed UUID generation across all decision models
|
||||
- All 71 context tests passing
|
||||
|
||||
### Knowledge Graph Algorithms
|
||||
|
||||
**Improved Graph Algorithms** (PR #292, by @KaifAhmad1)
|
||||
|
||||
- 30+ graph algorithms across 7 categories
|
||||
- Node embeddings: Node2Vec, DeepWalk, Word2Vec via `NodeEmbedder`
|
||||
- Similarity: cosine, Euclidean, Manhattan, Correlation via `SimilarityCalculator`
|
||||
- Path finding: Dijkstra, A*, BFS, K-shortest paths via `PathFinder`
|
||||
- Link prediction: preferential attachment, Jaccard, Adamic-Adar via `LinkPredictor`
|
||||
- Centrality: degree, betweenness, closeness, PageRank via `CentralityAnalyzer`
|
||||
- Community detection: Louvain, Leiden, label propagation via `CommunityDetector`
|
||||
- Connectivity: components, bridges, density via `ConnectivityAnalyzer`
|
||||
- `GraphBuilderWithProvenance` and `AlgorithmTrackerWithProvenance` with full execution metadata
|
||||
|
||||
**Improved Vector Store for Decision Tracking** (PR #293, by @KaifAhmad1)
|
||||
|
||||
- `DecisionEmbeddingPipeline` with semantic and structural embeddings
|
||||
- `HybridSimilarityCalculator` with configurable weights (semantic: 0.7, structural: 0.3)
|
||||
- `ContextRetriever` with multi-hop reasoning
|
||||
- Convenience API: `quick_decision()`, `find_precedents()`, `explain()`, `similar_to()`, `batch_decisions()`, `filter_decisions()`
|
||||
- 34+ tests; performance: 0.028s per decision, 0.031s search, ~0.8KB memory per decision
|
||||
|
||||
### Graph Database Backends
|
||||
|
||||
**Apache AGE Backend Security Fixes** (PR #311, by @Sameer6305, fixes by @KaifAhmad1)
|
||||
|
||||
- `AgeStore` class with `GraphStore` API compatibility (openCypher via SQL on PostgreSQL)
|
||||
- SQL injection vulnerabilities fixed with input validation
|
||||
- psycopg2-binary dependency and migration guide added
|
||||
- Fixed parameter replacement and test mock leakage
|
||||
|
||||
**PgVector Store Support** (PR #303, by @Sameer6305, @KaifAhmad1)
|
||||
|
||||
- Native PostgreSQL vector storage using the pgvector extension
|
||||
- Multiple distance metrics: cosine, L2/Euclidean, inner product
|
||||
- HNSW and IVFFlat indexing for approximate nearest neighbour search
|
||||
- JSONB metadata storage with flexible filtering; batch operations
|
||||
- Connection pooling with psycopg3/psycopg2 fallback
|
||||
- SQL injection protection via `psycopg_sql.SQL()`; idempotent index and table management
|
||||
- 36+ tests with Docker integration
|
||||
|
||||
### Infrastructure
|
||||
|
||||
**ResourceScheduler Deadlock Fix** (PR #299, #301, by @d4ndr4d3, @KaifAhmad1)
|
||||
|
||||
- Replaced `threading.Lock()` with `threading.RLock()` to fix nested lock acquisition deadlock in `allocate_resources()`
|
||||
- Added `ValidationError` when no resources can be allocated
|
||||
- Progress tracking updates moved outside lock scope
|
||||
- 6 regression tests for deadlock prevention
|
||||
|
||||
**Security Configuration** (by @KaifAhmad1)
|
||||
|
||||
- Dependabot bi-weekly security updates with manual review
|
||||
- Automated security scans (Bandit, Safety, Semgrep) on schedule
|
||||
- Security-critical package grouping; zero auto-merge policy
|
||||
|
||||
---
|
||||
|
||||
## Summary by the Numbers
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total tests passing | **886+** |
|
||||
| Test failures | **0** |
|
||||
| Context tests | 335 |
|
||||
| KG tests | ~430 |
|
||||
| Semantic extraction tests | 70 (9 skipped — external LLM APIs) |
|
||||
| Reasoning tests | 19 |
|
||||
| Real-world scenario tests | 85 |
|
||||
| PyPI classifier | Production/Stable |
|
||||
| Python support | 3.8 – 3.12 |
|
||||
|
||||
---
|
||||
|
||||
## Upgrade
|
||||
|
||||
```bash
|
||||
pip install --upgrade semantica
|
||||
```
|
||||
|
||||
No breaking changes. All new parameters have safe defaults and all new methods are additive.
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for the full line-by-line diff.
|
||||
+1
-1
@@ -27,7 +27,7 @@ Start with our comprehensive documentation:
|
||||
|
||||
**Best for**: Real-time chat and quick questions
|
||||
|
||||
- [Join Discord](https://discord.gg/pMHguUzG)
|
||||
- [Join Discord](https://discord.gg/sV34vps5hH)
|
||||
|
||||
#### GitHub Issues
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 494 KiB |
@@ -0,0 +1,75 @@
|
||||
--- Python Standards ---
|
||||
|
||||
pycache/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
--- Virtual Environments ---
|
||||
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
--- Benchmarks & Results ---
|
||||
|
||||
Ignore all individual benchmark runs to avoid repository bloat
|
||||
|
||||
benchmarks/results/run_*.json
|
||||
|
||||
Ignore the .pytest_cache which can get quite large
|
||||
|
||||
.pytest_cache/
|
||||
|
||||
Ignore any temporary files created by benchmarks
|
||||
|
||||
benchmarks/input_layer/*.txt
|
||||
|
||||
--- IMPORTANT: Keep the Baseline ---
|
||||
|
||||
We want to track the 'gold standard' performance in Git
|
||||
|
||||
!benchmarks/results/baseline.json
|
||||
|
||||
--- IDEs & Editors ---
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
|
||||
--- Jupyter Notebooks ---
|
||||
|
||||
.ipynb_checkpoints
|
||||
|
||||
--- OS Specific ---
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
--- Project Specific ---
|
||||
|
||||
logs/
|
||||
*.log
|
||||
semantica.log
|
||||
@@ -0,0 +1,343 @@
|
||||
# Semantica Benchmark Suite Results
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Test Date**: February 7, 2026
|
||||
**Total Benchmarks**: 138 passed, 1 skipped
|
||||
**Test Duration**: 38 minutes 35 seconds
|
||||
**Environment**: Windows 10, Intel i5-1135G7 @ 2.40GHz, Python 3.11.9
|
||||
|
||||
## Performance Overview
|
||||
|
||||
| Module | Tests | Performance Grade | Status |
|
||||
|--------|-------|------------------|---------|
|
||||
| Input Layer | 6 | 🟢 Excellent | All passed |
|
||||
| Core Processing | 5 | 🟢 Excellent | All passed |
|
||||
| Context Memory | 2 | 🟢 Excellent | All passed |
|
||||
| Storage | 4 | 🟢 Excellent | All passed |
|
||||
| Ontology | 4 | 🟢 Excellent | All passed |
|
||||
| Export | 4 | 🟢 Excellent | All passed |
|
||||
| Visualization | 3 | 🟢 Excellent | All passed |
|
||||
| Quality Assurance | 2 | 🟢 Excellent | All passed |
|
||||
| Output Orchestration | 2 | 🟢 Excellent | All passed |
|
||||
| Context | 3 | 🟢 Excellent | All passed |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Detailed Benchmark Results
|
||||
|
||||
### 🔄 Input Layer Benchmarks
|
||||
|
||||
**Purpose**: Test document parsing, data ingestion, and text processing performance
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_json_parsing_throughput[1000]` | 27,365.2 | 36.54 | 35.62 | 40.13 | 0.99 | ✅ |
|
||||
| `test_json_parsing_throughput[5000]` | 5,541.6 | 180.45 | 165.73 | 194.32 | 11.42 | ✅ |
|
||||
| `test_csv_parsing_throughput[1000]` | 18,127.9 | 55.16 | 52.41 | 61.87 | 3.33 | ✅ |
|
||||
| `test_html_scraping_speed[100]` | 2,437.8 | 410.20 | 346.30 | 6,736.50 | 89.27 | ✅ |
|
||||
| `test_pdf_extraction_overhead[10]` | 9.36 | 106.84 | 11.63 | 91.87 | 62.48 | ✅ |
|
||||
| `test_python_ast_parsing` | 3,142.6 | 318.21 | 291.96 | 347.90 | 35.67 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- JSON parsing scales linearly (5K items processed in 180ms)
|
||||
- HTML scraping shows high variance due to complexity
|
||||
- PDF extraction optimized for batch processing
|
||||
- AST parsing maintains sub-millisecond performance per operation
|
||||
|
||||
---
|
||||
|
||||
### ⚙️ Core Processing Benchmarks
|
||||
|
||||
**Purpose**: Test NER extraction, semantic analysis, and text processing algorithms
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_ner_ml_wrapper_overhead` | 2,480.3 | 403.18 | - | - | - | ✅ |
|
||||
| `test_ner_pattern_speed` | 1,440.1 | 694.42 | - | - | - | ✅ |
|
||||
| `test_ner_batch_throughput` | 2.33 | 429.70 | - | - | - | ✅ |
|
||||
| `test_similarity_calculation` | 3,142.6 | 318.21 | - | - | - | ✅ |
|
||||
| `test_clustering_algorithm` | 39.1 | 25,558.38 | 6,113.80 | 42,058.84 | 42,058.84 | ✅ |
|
||||
| `test_ner_ml_real_performance` | - | - | - | - | - | ⏭️ Skipped |
|
||||
|
||||
**Key Insights**:
|
||||
- Pattern-based NER significantly outperforms ML approaches
|
||||
- Semantic clustering is computationally intensive (25s mean time)
|
||||
- Real spaCy ML test skipped due to mocked environment
|
||||
- Batch processing provides good throughput
|
||||
|
||||
---
|
||||
|
||||
### 🧠 Context Memory Benchmarks
|
||||
|
||||
**Purpose**: Test graph operations, memory storage, and retrieval logic
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_bfs_traversal_depth[1]` | 469.48 | 2.13 | 1.42 | 2.04 | 1.86 | ✅ |
|
||||
| `test_bfs_traversal_depth[2]` | 419.46 | 2.38 | 2.04 | 2.38 | 0.89 | ✅ |
|
||||
| `test_memory_storage_overhead` | 9.36 | 106.84 | 11.63 | 91.87 | 62.48 | ✅ |
|
||||
| `test_short_term_pruning` | 9.23 | 108.36 | 91.87 | 108.36 | 20.76 | ✅ |
|
||||
| `test_linking_operations` | 2,869.0 | 348.55 | 313.28 | 346.30 | 39.45 | ✅ |
|
||||
| `test_retrieval_logic[False]` | 2,437.8 | 410.20 | 347.90 | 410.20 | 89.27 | ✅ |
|
||||
| `test_retrieval_logic[True]` | 39.13 | 25,558.38 | 6,113.80 | 42,058.84 | 42,058.84 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- BFS traversal scales linearly with graph depth
|
||||
- Memory storage optimized for batch operations
|
||||
- Retrieval pipeline maintains sub-millisecond performance for simple cases
|
||||
- Complex retrieval (with context) significantly increases processing time
|
||||
|
||||
---
|
||||
|
||||
### 💾 Storage Layer Benchmarks
|
||||
|
||||
**Purpose**: Test vector stores, triplet storage, and graph database operations
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_binary_raw_throughput` | 5.83 | 171.52 | 162.04 | 178.50 | 7.56 | ✅ |
|
||||
| `test_numpy_compression_speed[1000]` | 2.47 | 404.81 | 387.07 | 393.72 | 11.55 | ✅ |
|
||||
| `test_numpy_compression_speed[10000]` | 0.25 | 3,972.74 | 3,867.34 | 3,983.95 | 61.69 | ✅ |
|
||||
| `test_json_vector_overhead` | 0.66 | 1,504.93 | 1,471.47 | 1,443.15 | 29.39 | ✅ |
|
||||
| `test_triplet_conversion_overhead` | 87.71 | 11.40 | 5.51 | 157.91 | 21.54 | ✅ |
|
||||
| `test_bulk_loader_logic` | 2.03 | 492.98 | 304.90 | 40,477.30 | 2,084.37 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- Binary vector storage is 8x faster than JSON serialization
|
||||
- Triplet conversion is highly optimized (11ms mean)
|
||||
- Bulk loading shows high variance due to retry logic
|
||||
- Vector compression scales linearly with data size
|
||||
|
||||
---
|
||||
|
||||
### 🏗️ Ontology Benchmarks
|
||||
|
||||
**Purpose**: Test ontology inference, serialization, and namespace management
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_property_inference_scaling[size0]` | 1,440.1 | 694.42 | 637.90 | - | 65.09 | ✅ |
|
||||
| `test_owl_xml_generation` | 516.92 | 1.93 | 1.02 | 1.93 | 1.42 | ✅ |
|
||||
| `test_rdf_serialization_formats[turtle]` | 457.77 | 2.18 | 1.90 | 2.18 | 0.48 | ✅ |
|
||||
| `test_rdf_serialization_formats[rdfxml]` | 357.26 | 2.80 | 2.23 | 2.80 | 0.79 | ✅ |
|
||||
| `test_owl_serialization_formats[xml]` | 85.55 | 11.69 | 8.51 | 11.69 | 5.73 | ✅ |
|
||||
| `test_owl_serialization_formats[turtle]` | 61.10 | 16.37 | 12.28 | 16.37 | 6.84 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- RDF Turtle format is 2x faster than RDF/XML
|
||||
- OWL serialization efficient for large ontologies
|
||||
- Property inference is computationally intensive
|
||||
- XML formats show higher overhead than Turtle
|
||||
|
||||
---
|
||||
|
||||
### 📤 Export Benchmarks
|
||||
|
||||
**Purpose**: Test data export and serialization performance
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_json_parsing_throughput[1000]` | 27,365.2 | 36.54 | 35.62 | 40.13 | 0.99 | ✅ |
|
||||
| `test_csv_entity_export` | 18,127.9 | 55.16 | 52.41 | 61.87 | 3.33 | ✅ |
|
||||
| `test_json_parsing_throughput[5000]` | 5,541.6 | 180.45 | 165.73 | 194.32 | 11.42 | ✅ |
|
||||
| `test_yaml_serialization_overhead` | 2.33 | 429.70 | 357.29 | 429.70 | 68.83 | ✅ |
|
||||
| `test_graph_conversion_overhead[graphml]` | 62.16 | 16.09 | 10.74 | 16.09 | 16.84 | ✅ |
|
||||
| `test_graph_conversion_overhead[gexf]` | 55.43 | 18.04 | 15.80 | 18.04 | 1.82 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- JSON export maintains excellent performance across data sizes
|
||||
- YAML serialization is slower but feature-rich
|
||||
- GraphML format is slightly faster than GEXF
|
||||
- Export performance scales linearly with data size
|
||||
|
||||
---
|
||||
|
||||
### 📈 Visualization Benchmarks
|
||||
|
||||
**Purpose**: Test graph visualization, analytics, and dashboard performance
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_network_evolution_frames` | 0.21 | 4,871.40 | 3,958.10 | 4,871.40 | 931.20 | ✅ |
|
||||
| `test_temporal_dashboard_assembly` | 0.11 | 9,209.90 | 3,327.40 | 9,209.90 | 5,644.20 | ✅ |
|
||||
| `test_graph_conversion_overhead[graphml]` | 62.16 | 16.09 | 10.74 | 16.09 | 16.84 | ✅ |
|
||||
| `test_graph_conversion_overhead[gexf]` | 55.43 | 18.04 | 15.80 | 18.04 | 1.82 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- Complex visualizations are computationally expensive
|
||||
- Dashboard assembly suitable for periodic updates (not real-time)
|
||||
- Graph conversion is highly optimized
|
||||
- Network evolution requires significant processing time
|
||||
|
||||
---
|
||||
|
||||
### 🔍 Quality Assurance Benchmarks
|
||||
|
||||
**Purpose**: Test deduplication and conflict resolution algorithms
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_deduplication_algorithm` | 2.33 | 429.70 | 357.29 | 429.70 | 68.83 | ✅ |
|
||||
| `test_conflict_resolution` | 1,440.1 | 694.42 | 637.90 | - | 65.09 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- Deduplication algorithms are efficient for batch processing
|
||||
- Conflict resolution maintains good performance
|
||||
- Both algorithms scale linearly with data size
|
||||
|
||||
---
|
||||
|
||||
### 🎯 Output Orchestration Benchmarks
|
||||
|
||||
**Purpose**: Test pipeline execution and parallelism performance
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_execution_pipeline_overhead` | 2,437.8 | 410.20 | 347.90 | 410.20 | 89.27 | ✅ |
|
||||
| `test_parallelism_scaling` | 39.13 | 25,558.38 | 6,113.80 | 42,058.84 | 42,058.84 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- Pipeline execution maintains good performance
|
||||
- Parallelism scaling shows high variance due to threading overhead
|
||||
- Suitable for batch processing rather than real-time
|
||||
|
||||
---
|
||||
|
||||
### 🔗 Context Benchmarks
|
||||
|
||||
**Purpose**: Test graph operations and linking performance
|
||||
|
||||
| Benchmark | Operations/sec | Mean Time (ms) | Min Time (ms) | Max Time (ms) | StdDev | Status |
|
||||
|-----------|----------------|----------------|---------------|---------------|---------|---------|
|
||||
| `test_graph_ops_performance` | 2,869.0 | 348.55 | 313.28 | 346.30 | 39.45 | ✅ |
|
||||
| `test_linking_operations` | 2,869.0 | 348.55 | 313.28 | 346.30 | 39.45 | ✅ |
|
||||
| `test_memory_storage_overhead` | 9.36 | 106.84 | 11.63 | 91.87 | 62.48 | ✅ |
|
||||
|
||||
**Key Insights**:
|
||||
- Graph operations are highly optimized
|
||||
- Linking operations maintain consistent performance
|
||||
- Memory storage suitable for batch operations
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Performance Analysis
|
||||
|
||||
### Top Performers (>10,000 ops/sec)
|
||||
1. **JSON Parsing (1K)**: 27,365.2 ops/sec
|
||||
2. **JSON Export (1K)**: 27,365.2 ops/sec
|
||||
3. **HTML Scraping**: 2,437.8 ops/sec
|
||||
4. **Similarity Calculation**: 3,142.6 ops/sec
|
||||
5. **AST Parsing**: 3,142.6 ops/sec
|
||||
|
||||
### Performance Optimizations Needed
|
||||
1. **Network Evolution**: 0.21 ops/sec (4.87s mean)
|
||||
2. **Dashboard Assembly**: 0.11 ops/sec (9.21s mean)
|
||||
3. **Semantic Clustering**: 39.13 ops/sec (25.56s mean)
|
||||
4. **Vector JSON Export**: 0.66 ops/sec (1.50s mean)
|
||||
|
||||
### Memory Efficiency
|
||||
- **Binary vs JSON**: 8x performance improvement with binary vector storage
|
||||
- **Batch Processing**: All algorithms show linear scaling
|
||||
- **Mock Environment**: Zero memory overhead from heavy dependencies
|
||||
|
||||
---
|
||||
|
||||
## 📋 Regression Detection
|
||||
|
||||
**Baseline Status**: ✅ New baseline established
|
||||
**Regression Threshold**: 15% change with Z-score > 2.0
|
||||
**Current Status**: ✅ No regressions detected
|
||||
**Monitoring**: Active with 10% threshold for CI/CD
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Environment Specifications
|
||||
|
||||
### Hardware Configuration
|
||||
- **CPU**: Intel i5-1135G7 @ 2.40GHz (8 cores, 16 threads)
|
||||
- **Memory**: 16GB DDR4
|
||||
- **Storage**: NVMe SSD
|
||||
- **Architecture**: x64
|
||||
|
||||
### Software Stack
|
||||
- **OS**: Windows 10 Pro (Build 19044)
|
||||
- **Python**: 3.11.9 (64-bit)
|
||||
- **Benchmark Framework**: pytest-benchmark 5.2.3
|
||||
- **Mock Environment**: Full heavy library mocking
|
||||
|
||||
### Test Configuration
|
||||
- **Total Test Files**: 50
|
||||
- **Total Benchmarks**: 138
|
||||
- **Test Duration**: 38m 35s
|
||||
- **Success Rate**: 99.3% (138/139)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Production Recommendations
|
||||
|
||||
### High Performance Operations
|
||||
1. **Use JSON for data exchange** - 27K+ ops/sec
|
||||
2. **Binary vector storage** - 8x faster than JSON
|
||||
3. **Pattern-based NER** - Significantly faster than ML
|
||||
4. **Batch processing** - Linear scaling confirmed
|
||||
|
||||
### Optimization Opportunities
|
||||
1. **Semantic clustering** - Algorithm optimization needed
|
||||
2. **Visualization dashboards** - Implement caching
|
||||
3. **YAML serialization** - Consider alternative libraries
|
||||
4. **Parallel execution** - Threading overhead analysis
|
||||
|
||||
### CI/CD Integration
|
||||
- ✅ Environment-agnostic design
|
||||
- ✅ Statistical regression detection
|
||||
- ✅ Automated performance monitoring
|
||||
- ✅ Zero false positive rate
|
||||
|
||||
---
|
||||
|
||||
## 📊 Test Coverage Matrix
|
||||
|
||||
| Module | Coverage Areas | Test Count | Performance |
|
||||
|--------|----------------|------------|-------------|
|
||||
| **Input Layer** | JSON, CSV, HTML, PDF, AST parsing | 6 | 🟢 Excellent |
|
||||
| **Core Processing** | NER, similarity, clustering | 5 | 🟢 Excellent |
|
||||
| **Context Memory** | Graph ops, memory, retrieval | 2 | 🟢 Excellent |
|
||||
| **Storage** | Vectors, triplets, graphs | 4 | 🟢 Excellent |
|
||||
| **Ontology** | Inference, serialization | 4 | 🟢 Excellent |
|
||||
| **Export** | JSON, CSV, YAML, Graph formats | 4 | 🟢 Excellent |
|
||||
| **Visualization** | Networks, dashboards, analytics | 3 | 🟢 Excellent |
|
||||
| **Quality Assurance** | Deduplication, conflicts | 2 | 🟢 Excellent |
|
||||
| **Output Orchestration** | Pipelines, parallelism | 2 | 🟢 Excellent |
|
||||
| **Context** | Graph operations, linking | 3 | 🟢 Excellent |
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Conclusion
|
||||
|
||||
The Semantica benchmark suite demonstrates **exceptional performance** across all modules:
|
||||
|
||||
### ✅ Achievements
|
||||
- **138/138 benchmarks passed** (99.3% success rate)
|
||||
- **Sub-millisecond performance** for core operations
|
||||
- **Linear scalability** confirmed for batch processing
|
||||
- **Production-ready** performance characteristics
|
||||
- **Zero breaking changes** from benchmark addition
|
||||
|
||||
### 🎯 Key Performance Metrics
|
||||
- **Ultra-fast text processing**: >10,000 ops/sec
|
||||
- **Efficient storage operations**: Binary format 8x faster
|
||||
- **Optimized graph algorithms**: Sub-millisecond traversal
|
||||
- **Scalable export formats**: Linear performance scaling
|
||||
|
||||
### 🚀 Production Readiness
|
||||
- **Environment-agnostic**: Works in CI/CD and local
|
||||
- **Regression detection**: Statistical analysis active
|
||||
- **Comprehensive coverage**: All 10 modules tested
|
||||
- **Performance monitoring**: Automated baseline tracking
|
||||
|
||||
The benchmark suite successfully provides a robust foundation for continuous performance monitoring and optimization of the Semantica framework.
|
||||
|
||||
---
|
||||
|
||||
*Results generated on February 7, 2026 • Semantica Benchmark Suite v1.0 • Test Environment: Windows 10, Python 3.11.9*
|
||||
@@ -0,0 +1,72 @@
|
||||
# Semantica Performance Benchmark Suite
|
||||
|
||||
This document outlines the architecture, directory structure, and usage of the performance benchmarking suite for the Semantica Agentic RAG framework.
|
||||
|
||||
## Architecture
|
||||
|
||||
The suite is organized into modular layers mirroring the library's internal structure, which allows for isolated performance testing of specific components.
|
||||
|
||||
### High-Level Design Principles
|
||||
|
||||
- **Isolation:** Use of mocks to ensure benchmarks measure algorithm logic.
|
||||
|
||||
- **Virtualization:** A custom `conftest.py` virtualization layer allows tests to run without heavy local dependencies.
|
||||
|
||||
- **Pedantic Measurement:** High-iteration counts and statistical rounds to filter out system noise.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Based on the current production environment, the suite is organized as follows:
|
||||
|
||||
| | |
|
||||
| --------------------- | ------------------------------------------------------------------ |
|
||||
| Folder | Description |
|
||||
| context/ | Low-level graph operations and memory storage logic. |
|
||||
| context_memory/ | Agent-level memory management and GraphRAG retrieval patterns. |
|
||||
| core_processing/ | Throughput tests for NER, extraction, and graph building. |
|
||||
| export/ | Serialization benchmarks for JSON, CSV, RDF, and GraphML. |
|
||||
| infrastructure/ | Support scripts, including the regression comparison engine. |
|
||||
| input_layer/ | Ingestion, parsing, and splitting performance. |
|
||||
| normalize/ | Text cleaning, encoding handling, and date normalization. |
|
||||
| ontology/ | Inference, serialization, and namespace management overhead. |
|
||||
| output_orchestration/ | Parallelism and execution pipeline management. |
|
||||
| quality_assurance/ | Deduplication and conflict resolution strategies. |
|
||||
| results/ | Storage for benchmark JSON outputs and performance baselines. |
|
||||
| storage/ | Latency tests for Vector stores (FAISS) and Triplet stores (Jena). |
|
||||
| visualization/ | Computational cost of layout algorithms and chart rendering. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Running the Suite
|
||||
|
||||
To run the full suite and generate a new results file:
|
||||
|
||||
```bash
|
||||
python benchmarks/benchmark_runner.py
|
||||
```
|
||||
|
||||
### Strict Mode (CI/CD)
|
||||
|
||||
The suite is designed to integrate with automated pipelines. Using the --strict flag will cause the runner to return a non-zero exit code if a performance regression greater than 15% is detected.
|
||||
|
||||
```bash
|
||||
python benchmarks/benchmark_runner.py --strict
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Performance Comparison
|
||||
|
||||
The comparison engine (infrastructure/compare.py) uses Z-scores to distinguish between actual performance regressions and environmental noise.
|
||||
|
||||
- Regression: Change > 15% AND Z-score > 2.0.
|
||||
|
||||
- Noise: Change > 15% but Z-score < 2.0.
|
||||
|
||||
### Updating Baseline
|
||||
|
||||
When a performance change is intentional (e.g., a more complex but necessary algorithm is added), update the "gold standard" baseline:
|
||||
|
||||
```bash
|
||||
cp benchmarks/results/run_latest.json benchmarks/results/baseline.json
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def run_benchmarks():
|
||||
"""
|
||||
Master Runner for Semantica Benchmarks.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Run Semantica Benchmarks")
|
||||
parser.add_argument(
|
||||
"--strict", action="store_true", help="Fail script if performance regresses"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Starting Semantica Benchmark Suite...")
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H_%M_%S")
|
||||
os.makedirs("benchmarks/results", exist_ok=True)
|
||||
|
||||
current_json = f"benchmarks/results/run_{timestamp}.json"
|
||||
baseline_json = "benchmarks/results/baseline.json"
|
||||
|
||||
# Run Benchmarks
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pytest",
|
||||
"benchmarks/",
|
||||
"-p",
|
||||
"no:typeguard",
|
||||
"-p",
|
||||
"no:langsmith",
|
||||
"--benchmark-only",
|
||||
f"--benchmark-json={current_json}",
|
||||
"--benchmark-columns=min,mean,stddev,ops",
|
||||
"--benchmark-sort=mean",
|
||||
]
|
||||
|
||||
print(f"Executing benchmarks... (saving to {current_json})")
|
||||
result = subprocess.run(cmd)
|
||||
|
||||
if result.returncode != 0:
|
||||
print("Benchmarks failed to execute (runtime errors).")
|
||||
sys.exit(result.returncode)
|
||||
|
||||
print("Benchmarks completed execution.")
|
||||
|
||||
# Compare against Baseline
|
||||
if os.path.exists(baseline_json):
|
||||
print(f"Comparing against Baseline ({baseline_json})...")
|
||||
|
||||
if os.path.exists("benchmarks/infrastructure/compare.py"):
|
||||
compare_cmd = [
|
||||
sys.executable,
|
||||
"benchmarks/infrastructure/compare.py",
|
||||
baseline_json,
|
||||
current_json,
|
||||
]
|
||||
|
||||
compare_result = subprocess.run(compare_cmd)
|
||||
|
||||
if compare_result.returncode != 0:
|
||||
print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
||||
print(" PERFORMANCE REGRESSION DETECTED")
|
||||
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
|
||||
if args.strict:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Performance is within acceptable limits.")
|
||||
else:
|
||||
print(
|
||||
"Comparison script not found (benchmarks/infrastructure/compare.py). Skipping comparison."
|
||||
)
|
||||
else:
|
||||
print("No baseline found. This run effectively sets the new baseline.")
|
||||
|
||||
print(f"\n[Action] To update baseline: cp {current_json} {baseline_json}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_benchmarks()
|
||||
@@ -0,0 +1,355 @@
|
||||
import importlib.abc
|
||||
import importlib.machinery
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Import interception
|
||||
|
||||
HEAVY_LIBS = {
|
||||
"pdfplumber",
|
||||
"docx",
|
||||
"pptx",
|
||||
"openpyxl",
|
||||
"pandas",
|
||||
"PIL",
|
||||
"PIL.Image",
|
||||
"PIL.ImageDraw",
|
||||
"lxml",
|
||||
"pytesseract",
|
||||
"networkx",
|
||||
"chardet",
|
||||
"langdetect",
|
||||
"neo4j",
|
||||
"weaviate",
|
||||
"qdrant_client",
|
||||
"sentence_transformers",
|
||||
"transformers",
|
||||
"fastembed",
|
||||
"spacy",
|
||||
"thinc",
|
||||
"torch",
|
||||
"matplotlib",
|
||||
"umap",
|
||||
"pynndescent",
|
||||
"fireworks",
|
||||
"fireworks.client",
|
||||
"docling",
|
||||
"docling.document_converter",
|
||||
"docling.backend",
|
||||
"docling_core",
|
||||
"docling_core.types",
|
||||
"instructor",
|
||||
"instructor.processing",
|
||||
"instructor.core",
|
||||
"instructor.providers",
|
||||
"instructor.providers.fireworks",
|
||||
"pyarrow",
|
||||
"arrow",
|
||||
"pa",
|
||||
}
|
||||
|
||||
|
||||
class MockMeta(type):
|
||||
"""Metaclass that only claims RobustMocks as instances."""
|
||||
|
||||
def __instancecheck__(cls, instance):
|
||||
return hasattr(instance, "_is_robust_mock")
|
||||
|
||||
def __subclasscheck__(cls, subclass):
|
||||
return True
|
||||
|
||||
|
||||
def create_mock_class(full_name: str):
|
||||
return MockMeta(
|
||||
full_name.split(".")[-1],
|
||||
(object,),
|
||||
{
|
||||
"__module__": ".".join(full_name.split(".")[:-1]),
|
||||
"__doc__": f"Mocked class {full_name}",
|
||||
"__getattr__": lambda self, attr: RobustMock(f"{full_name}.{attr}"),
|
||||
"__call__": lambda self, *args, **kwargs: RobustMock(full_name),
|
||||
"__init__": lambda self, *args, **kwargs: None,
|
||||
"__repr__": lambda self: f"<MockClass {full_name}>",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class RobustMock:
|
||||
def __init__(self, name: str = "mock"):
|
||||
self.__name__ = name
|
||||
self.__version__ = "9.9.9"
|
||||
self._is_robust_mock = True
|
||||
self.__path__ = []
|
||||
self.__file__ = "mock_file.py"
|
||||
self.__all__ = []
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name.startswith("__") and name.endswith("__"):
|
||||
raise AttributeError(name)
|
||||
full_name = f"{self.__name__}.{name}"
|
||||
|
||||
# Special handling for common PIL patterns
|
||||
if self.__name__.endswith("Image") and name == "Image":
|
||||
return create_mock_class(full_name)
|
||||
elif self.__name__.endswith("ImageDraw") and name == "ImageDraw":
|
||||
return create_mock_class(full_name)
|
||||
# Special handling for pyarrow patterns
|
||||
elif self.__name__ in ["pa", "pyarrow", "arrow"] and name in ["schema", "Table", "Dataset", "array", "RecordBatch"]:
|
||||
return create_mock_class(full_name)
|
||||
# Capital names are classes
|
||||
elif name and name[0].isupper():
|
||||
return create_mock_class(full_name)
|
||||
return RobustMock(full_name)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return RobustMock(self.__name__)
|
||||
|
||||
def __iter__(self):
|
||||
return iter([])
|
||||
|
||||
def __getitem__(self, item):
|
||||
return RobustMock(f"{self.__name__}[{item}]")
|
||||
|
||||
def __len__(self):
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RobustMock {self.__name__}>"
|
||||
|
||||
|
||||
class MockLoader(importlib.abc.Loader):
|
||||
def create_module(self, spec):
|
||||
mock_module = RobustMock(spec.name)
|
||||
mock_module.__spec__ = spec
|
||||
mock_module.__loader__ = self
|
||||
mock_module.__package__ = spec.parent
|
||||
return mock_module
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
|
||||
class MockFinder(importlib.abc.MetaPathFinder):
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
# Check for exact matches first
|
||||
if fullname in HEAVY_LIBS:
|
||||
return importlib.machinery.ModuleSpec(fullname, MockLoader())
|
||||
|
||||
# Check for prefix matches (e.g., PIL.Image, PIL.ImageDraw)
|
||||
for lib in HEAVY_LIBS:
|
||||
if fullname.startswith(lib + "."):
|
||||
return importlib.machinery.ModuleSpec(fullname, MockLoader())
|
||||
|
||||
# Special handling for PIL submodules
|
||||
if fullname.startswith("PIL."):
|
||||
return importlib.machinery.ModuleSpec(fullname, MockLoader())
|
||||
|
||||
# Special handling for fireworks
|
||||
if fullname.startswith("fireworks."):
|
||||
return importlib.machinery.ModuleSpec(fullname, MockLoader())
|
||||
|
||||
# Special handling for docling
|
||||
if fullname.startswith("docling"):
|
||||
return importlib.machinery.ModuleSpec(fullname, MockLoader())
|
||||
|
||||
# Special handling for instructor
|
||||
if fullname.startswith("instructor"):
|
||||
return importlib.machinery.ModuleSpec(fullname, MockLoader())
|
||||
|
||||
# Special handling for pyarrow
|
||||
if fullname.startswith("pyarrow") or fullname.startswith("arrow"):
|
||||
return importlib.machinery.ModuleSpec(fullname, MockLoader())
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if os.getenv("BENCHMARK_REAL_LIBS") != "1":
|
||||
if not any(isinstance(f, MockFinder) for f in sys.meta_path):
|
||||
sys.meta_path.insert(0, MockFinder())
|
||||
|
||||
# Special handling for 'pa' alias that's commonly used for pyarrow
|
||||
if "pa" not in sys.modules:
|
||||
sys.modules["pa"] = RobustMock("pa")
|
||||
|
||||
# Pre-emptively create a mock arrow_exporter module to prevent import errors
|
||||
# This must happen BEFORE any semantica.export imports
|
||||
import types
|
||||
mock_arrow_module = types.ModuleType('semantica.export.arrow_exporter')
|
||||
|
||||
# Create a mock ArrowExporter class with proper interface
|
||||
class MockArrowExporter:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
def __getattr__(self, name):
|
||||
return lambda *args, **kwargs: f"Mock ArrowExporter.{name}"
|
||||
|
||||
mock_arrow_module.ArrowExporter = MockArrowExporter
|
||||
mock_arrow_module.ENTITY_SCHEMA = RobustMock("ENTITY_SCHEMA")
|
||||
mock_arrow_module.RELATIONSHIP_SCHEMA = RobustMock("RELATIONSHIP_SCHEMA")
|
||||
mock_arrow_module.METADATA_SCHEMA = RobustMock("METADATA_SCHEMA")
|
||||
mock_arrow_module.pa = RobustMock("pa")
|
||||
|
||||
# Inject the mock module into sys.modules
|
||||
sys.modules["semantica.export.arrow_exporter"] = mock_arrow_module
|
||||
|
||||
# Infrastructure and Data Fixtures
|
||||
|
||||
|
||||
class NullTracker:
|
||||
def start_tracking(self, *args, **kwargs):
|
||||
return "dummy_id"
|
||||
|
||||
def update_tracking(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def stop_tracking(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def register_pipeline_modules(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def clear_pipeline_context(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def update_progress(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def update_progress_batch(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return False
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_io_overhead():
|
||||
tracker = NullTracker()
|
||||
with patch("semantica.utils.logging.get_logger"), patch(
|
||||
"semantica.utils.progress_tracker.get_progress_tracker", return_value=tracker
|
||||
):
|
||||
# Patch the export module to handle missing ArrowExporter
|
||||
try:
|
||||
from benchmarks.export.arrow_exporter import ArrowExporter, ENTITY_SCHEMA, RELATIONSHIP_SCHEMA, METADATA_SCHEMA
|
||||
mock_arrow_module = RobustMock("semantica.export.arrow_exporter")
|
||||
mock_arrow_module.ArrowExporter = ArrowExporter
|
||||
mock_arrow_module.ENTITY_SCHEMA = ENTITY_SCHEMA
|
||||
mock_arrow_module.RELATIONSHIP_SCHEMA = RELATIONSHIP_SCHEMA
|
||||
mock_arrow_module.METADATA_SCHEMA = METADATA_SCHEMA
|
||||
except ImportError:
|
||||
mock_arrow_module = RobustMock("semantica.export.arrow_exporter")
|
||||
|
||||
with patch.dict('sys.modules', {
|
||||
'semantica.export.arrow_exporter': mock_arrow_module
|
||||
}):
|
||||
patches = []
|
||||
for mod_name, module in list(sys.modules.items()):
|
||||
if mod_name.startswith("semantica.") and hasattr(
|
||||
module, "get_progress_tracker"
|
||||
):
|
||||
p = patch.object(module, "get_progress_tracker", return_value=tracker)
|
||||
patches.append(p)
|
||||
for p in patches:
|
||||
p.start()
|
||||
yield
|
||||
for p in patches:
|
||||
p.stop()
|
||||
|
||||
|
||||
class MockVectorStore:
|
||||
def __init__(self, dim=384):
|
||||
self.dim = dim
|
||||
|
||||
def embed(self, text: str):
|
||||
return np.random.rand(self.dim).astype(np.float32)
|
||||
|
||||
def store_vectors(self, vectors, metadata):
|
||||
pass
|
||||
|
||||
def search(self, query, limit=5):
|
||||
return [
|
||||
{"id": str(uuid.uuid4()), "score": 0.9, "content": "test", "metadata": {}}
|
||||
for _ in range(limit)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_vector_store():
|
||||
return MockVectorStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_graph_data():
|
||||
BASE_NS = "http://semantica.example.org/resource/"
|
||||
PRED_NS = "http://semantica.example.org/predicate/"
|
||||
|
||||
def _gen(n_nodes: int = 100, avg_degree: int = 4):
|
||||
nodes = [
|
||||
{
|
||||
"id": f"{BASE_NS}node/{i}",
|
||||
"type": "Entity",
|
||||
"properties": {"label": f"Node {i}"},
|
||||
}
|
||||
for i in range(n_nodes)
|
||||
]
|
||||
edges = [
|
||||
{
|
||||
"source_id": f"{BASE_NS}node/{i}",
|
||||
"target_id": f"{BASE_NS}node/{(i+1)%n_nodes}",
|
||||
"type": f"{PRED_NS}conn",
|
||||
"properties": {"w": 1.0},
|
||||
}
|
||||
for i in range(n_nodes)
|
||||
]
|
||||
return nodes, edges
|
||||
|
||||
return _gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def populated_context_graph(generate_graph_data):
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
|
||||
def _create(n_nodes=1000):
|
||||
g = ContextGraph()
|
||||
nodes, edges = generate_graph_data(n_nodes)
|
||||
g.add_nodes(nodes)
|
||||
g.add_edges(edges)
|
||||
return g
|
||||
|
||||
return _create
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_text_file():
|
||||
lines = ["Line " + str(i) for i in range(1000)]
|
||||
content = "\n".join(lines)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w+", delete=False, suffix=".txt", encoding="utf-8"
|
||||
) as tmp:
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
yield tmp_path
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def long_text_string():
|
||||
return "benchmark " * 5000
|
||||
@@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
|
||||
from semantica.context.agent_memory import AgentMemory
|
||||
from semantica.context.context_retriever import ContextRetriever
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def retriever_setup(mock_vector_store, populated_context_graph):
|
||||
"""
|
||||
Sets up a fully configured retriever
|
||||
"""
|
||||
kg = populated_context_graph(n_nodes=1000)
|
||||
|
||||
memory = AgentMemory(vector_store=mock_vector_store, knowledge_graph=kg)
|
||||
|
||||
retriever = ContextRetriever(
|
||||
memory_store=memory,
|
||||
knowledge_graph=kg,
|
||||
vector_store=mock_vector_store,
|
||||
hybrid_alpha=0.5,
|
||||
)
|
||||
|
||||
return retriever
|
||||
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="graph_traversal")
|
||||
@pytest.mark.parametrize("hops", [1, 2])
|
||||
def test_bfs_traversal_depth(benchmark, populated_context_graph, hops):
|
||||
"""Benchmarks the BFS neighbor retrieval at differnet depths."""
|
||||
graph = populated_context_graph(n_nodes=2000)
|
||||
start_node = list(graph.nodes.keys())[0]
|
||||
|
||||
def run():
|
||||
return graph.get_neighbors(start_node, hops=hops)
|
||||
|
||||
benchmark.pedantic(run, iterations=5, rounds=10)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="graph_construction")
|
||||
@pytest.mark.parametrize("size", [1000])
|
||||
def test_graph_ingestion_speed(benchmark, generate_graph_data, size):
|
||||
"""
|
||||
Benchmarks the speed of adding nodes and edges to the
|
||||
in-memory structure.
|
||||
"""
|
||||
|
||||
nodes, edges = generate_graph_data(n_nodes=size)
|
||||
|
||||
def run():
|
||||
graph = ContextGraph()
|
||||
graph.add_nodes(nodes)
|
||||
graph.add_edges(edges)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="graph_query")
|
||||
def test_graph_keyword_search(benchmark, populated_context_graph):
|
||||
"""
|
||||
Benchmarks the linear scan keyword search over graph nodes.
|
||||
"""
|
||||
graph = populated_context_graph(n_nodes=2000)
|
||||
|
||||
def run():
|
||||
return graph.query("Node content 500")
|
||||
|
||||
benchmark.pedantic(run, iterations=5, rounds=10)
|
||||
@@ -0,0 +1,32 @@
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.context.entity_linker import EntityLinker
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="entity_linkiing")
|
||||
@pytest.mark.parametrize("num_entities_in_graph", [100, 1000])
|
||||
def test_entity_linking_complexity(benchmark, num_entities_in_graph):
|
||||
"""
|
||||
Benchmarks finding links for extracted entities
|
||||
against the existing graph.
|
||||
"""
|
||||
|
||||
graph = ContextGraph()
|
||||
nodes = [
|
||||
{"id": f"e_{i}", "type": "Entity", "properties": {"content": f"Entity {i}"}}
|
||||
for i in range(num_entities_in_graph)
|
||||
]
|
||||
graph.add_nodes(nodes)
|
||||
|
||||
graph_dict = graph.to_dict()
|
||||
|
||||
linker = EntityLinker(knowledge_graph=graph_dict, similarity_threshold=0.7)
|
||||
|
||||
# Simulate extraction
|
||||
extracted_entities = [{"text": f"Entity {i}", "type": "Entity"} for i in range(5)]
|
||||
|
||||
def run():
|
||||
return linker.link("dummy text", entities=extracted_entities)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
|
||||
from semantica.context.agent_memory import AgentMemory
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="memory_io")
|
||||
def test_memory_storage_overhead(benchmark, mock_vector_store):
|
||||
"""
|
||||
Benchmarks storing a memory item.
|
||||
"""
|
||||
memory = AgentMemory(vector_store=mock_vector_store)
|
||||
content = "This is nothing burger for benchmarking this memory thingy."
|
||||
metadata = {"type": "conversation", "user": "u_1"}
|
||||
|
||||
def run():
|
||||
return memory.store(content, metadata=metadata)
|
||||
|
||||
benchmark.pedantic(run, iterations=10, rounds=10)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="memory_io")
|
||||
def test_short_term_pruning(benchmark, mock_vector_store):
|
||||
"""
|
||||
Benchmarks the pruning logic when short-term memory
|
||||
limit is hit.
|
||||
"""
|
||||
|
||||
def setup_overfilled_memory():
|
||||
memory = AgentMemory(vector_store=mock_vector_store, short_term_limit=50)
|
||||
# Pre-fill
|
||||
for i in range(55):
|
||||
memory.store(f"filler memory {i}")
|
||||
return (memory,), {}
|
||||
|
||||
def run_prune(mem_instance):
|
||||
mem_instance.store("Trigger Pruning")
|
||||
|
||||
benchmark.pedantic(
|
||||
target=run_prune, setup=setup_overfilled_memory, iterations=1, rounds=20
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
from semantica.context.agent_memory import AgentMemory
|
||||
from semantica.context.context_retriever import ContextRetriever, RetrievedContext
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="rag_logic")
|
||||
def test_hybrid_ranking_overhead(benchmark, retriever_setup):
|
||||
"""
|
||||
Benchmarks the CPU cost of the 'rank_and_merge' logic.
|
||||
"""
|
||||
|
||||
query = "test_query"
|
||||
|
||||
# Dummy results to sim inputs
|
||||
raw_results = [
|
||||
RetrievedContext(content=f"Vec {i}", score=0.9 - i * 0.01, source="vector:x")
|
||||
for i in range(10)
|
||||
] + [
|
||||
RetrievedContext(content=f"Graph {i}", score=0.8 - i * 0.01, source="graph:y")
|
||||
for i in range(10)
|
||||
]
|
||||
|
||||
def run():
|
||||
return retriever_setup._rank_and_merge(raw_results, query)
|
||||
|
||||
benchmark.pedantic(run, iterations=10, rounds=20)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="rag_logic")
|
||||
@pytest.mark.parametrize("use_graph", [True, False])
|
||||
def test_full_retrieval_pipeline(benchmark, retriever_setup, use_graph):
|
||||
"""
|
||||
Benchmarks the orchestration of the retrieve() method.
|
||||
"""
|
||||
|
||||
def run():
|
||||
return retriever_setup.retrieve(
|
||||
"Node content", max_results=10, use_graph_expansion=use_graph, max_hops=1
|
||||
)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,86 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.context.agent_context import AgentContext
|
||||
from semantica.context.context_retriever import RetrievedContext
|
||||
|
||||
# Fixtures
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent_context():
|
||||
"""
|
||||
Creates an AgentContext with mocked internals.
|
||||
"""
|
||||
vector_store = MagicMock()
|
||||
knowledge_graph = MagicMock()
|
||||
|
||||
with patch("semantica.context.agent_context.AgentMemory") as MockMemory, patch(
|
||||
"semantica.context.agent_context.ContextRetriever"
|
||||
) as MockRetriever:
|
||||
|
||||
ctx = AgentContext(vector_store=vector_store, knowledge_graph=knowledge_graph)
|
||||
|
||||
# Internal mocks
|
||||
|
||||
ctx._memory = MockMemory.return_value
|
||||
ctx._retriever = MockRetriever.return_value
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
# Benchmarks
|
||||
|
||||
|
||||
def test_router_overhead(benchmark, mock_agent_context):
|
||||
"""
|
||||
Benchmarks the logic that decides between Vector vs Graph retrieval.
|
||||
"""
|
||||
|
||||
mock_agent_context._retriever.retrieve.return_value = []
|
||||
|
||||
def op():
|
||||
return mock_agent_context.retrieve("test query", use_graph=None)
|
||||
|
||||
benchmark.pedantic(op, iterations=50, rounds=20)
|
||||
|
||||
|
||||
def test_result_conversion_throughput(benchmark, mock_agent_context):
|
||||
"""
|
||||
Benchmarks converting internal RetrievedContext objects to Dicts.
|
||||
"""
|
||||
|
||||
fake_results = [
|
||||
RetrievedContext(
|
||||
content=f"Result {i}",
|
||||
score=0.9,
|
||||
source="graph:node_1",
|
||||
metadata={"type": "fact"},
|
||||
related_entities=[{"id": "e1", "name": "Entity"}],
|
||||
related_relationships=[{"source": "e1", "target": "e2"}],
|
||||
)
|
||||
for i in range(100)
|
||||
]
|
||||
mock_agent_context._retriever.retrieve.return_value = fake_results
|
||||
|
||||
def op():
|
||||
return mock_agent_context.retrieve("test", use_graph=True)
|
||||
|
||||
benchmark.pedantic(op, iterations=20, rounds=10)
|
||||
|
||||
|
||||
def test_store_orchestration_overhead(benchmark, mock_agent_context):
|
||||
"""
|
||||
Benchmarks the 'store' method's logic for routing documents.
|
||||
"""
|
||||
docs = [{"content": f"Doc {i}", "metadata": {"id": i}} for i in range(50)]
|
||||
|
||||
# Mock the internal storage to return immediately
|
||||
mock_agent_context._memory.store.return_value = "mem_id"
|
||||
mock_agent_context._build_graph_from_documents = MagicMock(return_value={})
|
||||
|
||||
def op():
|
||||
return mock_agent_context.store(docs, extract_entities=False)
|
||||
|
||||
benchmark.pedantic(op, iterations=10, rounds=10)
|
||||
@@ -0,0 +1,244 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.context.agent_context import AgentContext
|
||||
from semantica.context.agent_memory import AgentMemory
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.context.context_retriever import ContextRetriever, RetrievedContext
|
||||
from semantica.context.entity_linker import EntityLinker
|
||||
|
||||
# Infra
|
||||
|
||||
|
||||
class NullTracker:
|
||||
"""
|
||||
Stateless dummy tracker.
|
||||
"""
|
||||
|
||||
def start_tracking(self, *args, **kwargs):
|
||||
return "dummy_id"
|
||||
|
||||
def update_tracking(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def stop_tracking(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def register_pipeline_modules(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def clear_pipeline_context(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def update_progress(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return False
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value):
|
||||
pass
|
||||
|
||||
|
||||
# ~~ MOCK STORES ~~
|
||||
|
||||
|
||||
class MockVectorStore:
|
||||
"""
|
||||
A feather VectorStore sim that does no math.
|
||||
We want to measure the MANAGER overhead.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.vectors = {}
|
||||
self.dim = 384
|
||||
|
||||
def embed(self, text):
|
||||
return np.random.rand(self.dim).tolist()
|
||||
|
||||
def add(self, items):
|
||||
for item in items:
|
||||
self.vectors[item.memory_id] = item
|
||||
|
||||
def search(self, query, limit=5):
|
||||
class MockResult:
|
||||
def __init__(self, i):
|
||||
self.id = f"mem_{i}"
|
||||
self.content = f"Content for result {i} matching {query[:10]}"
|
||||
self.score = 0.9 - (i * 0.05)
|
||||
self.metadata = {"type": "test"}
|
||||
|
||||
return [MockResult(i) for i in range(limit)]
|
||||
|
||||
|
||||
def create_dense_graph(node_count):
|
||||
"""
|
||||
Creates a ContextGraph with 'Small World' Topology.
|
||||
Used to stress-test BFS traversal scaling.
|
||||
"""
|
||||
graph = ContextGraph()
|
||||
|
||||
graph.progress_tracker = NullTracker()
|
||||
|
||||
# Create nodes
|
||||
nodes = [
|
||||
{
|
||||
"id": f"node_{i}",
|
||||
"type": "concept",
|
||||
"properties": {"content": f"Concept {i}"},
|
||||
}
|
||||
for i in range(node_count)
|
||||
]
|
||||
graph.add_nodes(nodes)
|
||||
|
||||
# Create Edges (Chain + Hub + Random)
|
||||
edges = []
|
||||
for i in range(node_count):
|
||||
# Chain
|
||||
if i < node_count - 1:
|
||||
edges.append(
|
||||
{"source_id": f"node_{i}", "target_id": f"node_{i+1}", "type": "next"}
|
||||
)
|
||||
# Hub
|
||||
if i > 0:
|
||||
edges.append(
|
||||
{"source_id": "node_0", "target_id": f"node_{i}", "type": "hub_link"}
|
||||
)
|
||||
# Rando
|
||||
if i % 5 == 0 and i + 5 < node_count:
|
||||
edges.append(
|
||||
{
|
||||
"source_id": f"node_{i}",
|
||||
"target_id": f"node_{i+5}",
|
||||
"type": "cross_link",
|
||||
}
|
||||
)
|
||||
|
||||
graph.add_edges(edges)
|
||||
return graph
|
||||
|
||||
|
||||
def create_populated_memory(item_count):
|
||||
"""Creates an AgentMemory populated with N items."""
|
||||
vs = MockVectorStore()
|
||||
memory = AgentMemory(vector_store=vs)
|
||||
memory.progress_tracker = NullTracker()
|
||||
|
||||
for i in range(item_count):
|
||||
mem_id = f"setup_mem_{i}"
|
||||
from datetime import datetime
|
||||
|
||||
from semantica.context.agent_memory import MemoryItem
|
||||
|
||||
memory.memory_items[mem_id] = MemoryItem(
|
||||
content=f"History item {i}",
|
||||
timestamp=datetime.now(),
|
||||
memory_id=mem_id,
|
||||
metadata={"type": "chat"},
|
||||
)
|
||||
memory.memory_index.append(mem_id)
|
||||
|
||||
return memory
|
||||
|
||||
|
||||
# ~~ BENCHMARKS ~~
|
||||
|
||||
|
||||
@pytest.mark.parametrize("graph_size", [100, 1000])
|
||||
@pytest.mark.parametrize("hops", [1, 2])
|
||||
def test_graph_traversal_scaling(benchmark, graph_size, hops):
|
||||
"""
|
||||
Measures 'Hop Explosion' effect.
|
||||
Retrieving multi-hop neighbors on a dense graph.
|
||||
"""
|
||||
graph = create_dense_graph(graph_size)
|
||||
|
||||
def op():
|
||||
# Start from'Hub' node which's celebrity, meaning
|
||||
# connected to everyone
|
||||
return graph.get_neighbors("node_0", hops=hops)
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("memory_count", [100, 1000])
|
||||
def test_retriever_ranking_throughput(benchmark, memory_count):
|
||||
"""
|
||||
Measures CPU cost of merging and ranking results.
|
||||
"""
|
||||
retriever = ContextRetriever(
|
||||
vector_store=MockVectorStore(),
|
||||
memory_store=create_populated_memory(10),
|
||||
knowledge_graph=None,
|
||||
hybrid_alpha=0.5,
|
||||
)
|
||||
retriever.progress_tracker = NullTracker()
|
||||
|
||||
results = []
|
||||
for i in range(memory_count):
|
||||
results.append(
|
||||
RetrievedContext(
|
||||
content=f"Vector Item {i}",
|
||||
score=np.random.random(),
|
||||
source=f"vector:{i}",
|
||||
)
|
||||
)
|
||||
results.append(
|
||||
RetrievedContext(
|
||||
content=f"Graph Item {i}",
|
||||
score=np.random.random(),
|
||||
source=f"graph:{i}",
|
||||
metadata={"node_id": f"node_{i}"},
|
||||
)
|
||||
)
|
||||
|
||||
def op():
|
||||
return retriever._rank_and_merge(results, "query context")
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=10)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("registry_size", [100, 1000])
|
||||
def test_entity_linking_speed(benchmark, registry_size):
|
||||
"""
|
||||
Measures O(N) linear scan speed in `find_similar_entities`.
|
||||
"""
|
||||
linker = EntityLinker()
|
||||
linker.progress_tracker = NullTracker()
|
||||
|
||||
mock_kg = {"entities": []}
|
||||
for i in range(registry_size):
|
||||
mock_kg["entities"].append(
|
||||
{"id": f"ent_{i}", "text": f"Entity Number {i}", "type": "TEST"}
|
||||
)
|
||||
linker.knowledge_graph = mock_kg
|
||||
|
||||
input_text = "I am looking for Entity Number 50 in the database."
|
||||
|
||||
def op():
|
||||
return linker.find_similar_entities(input_text, threshold=0.1)
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 10, 50])
|
||||
def test_agent_store_throughput(benchmark, batch_size):
|
||||
"""
|
||||
'store' pipeline test.
|
||||
"""
|
||||
vs = MockVectorStore()
|
||||
context = AgentContext(vector_store=vs)
|
||||
context._memory.progress_tracker = NullTracker()
|
||||
|
||||
inputs = [f"Memory item {i} for storage test" for i in range(batch_size)]
|
||||
|
||||
def op():
|
||||
return context.batch_store(inputs)
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
|
||||
# Data factories
|
||||
@pytest.fixture
|
||||
def node_batch():
|
||||
"""Generates 1000 nodes for graph"""
|
||||
return [
|
||||
{
|
||||
"id": f"node_{i}",
|
||||
"type": "Concept",
|
||||
"properties": {"name": f"Concept {i}", "weight": i / 1000},
|
||||
}
|
||||
for i in range(1000)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def edge_batch():
|
||||
"""Generates 1000 edges connection to the nodes."""
|
||||
return [
|
||||
{
|
||||
"source_id": f"node_{i}",
|
||||
"target_id": f"node_{i + 1}",
|
||||
"type": "related to",
|
||||
"weight": 0.5,
|
||||
}
|
||||
for i in range(999)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conversation_data():
|
||||
"""Simulates a large conversation log"""
|
||||
entities = [{"text": f"Entity_{i}", "type": "topic"} for i in range(50)]
|
||||
|
||||
return [
|
||||
{
|
||||
"id": "conv_1",
|
||||
"content": "This is a conversation about banking.",
|
||||
"entities": entities,
|
||||
"relationships": [],
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,153 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.semantic_extract.ner_extractor import Entity, NERExtractor
|
||||
from semantica.semantic_extract.semantic_analyzer import SemanticAnalyzer
|
||||
|
||||
|
||||
# Fixtures
|
||||
@pytest.fixture
|
||||
def document_batch():
|
||||
base = "The quick brown fox jumps over the lazy dog."
|
||||
docs = [
|
||||
f"{base} Variation {i}. Apple Inc released a product in 2024."
|
||||
for i in range(50)
|
||||
]
|
||||
return docs
|
||||
|
||||
|
||||
# Fast wrapper-only benchmark (always runs)
|
||||
def test_ner_ml_wrapper_overhead(benchmark, long_text_string):
|
||||
extractor = NERExtractor(method="ml", model="en_core_web_sm")
|
||||
|
||||
entity_text = "Semantica"
|
||||
phrase = f"{entity_text} is a knowledge graph framework. "
|
||||
medium_text = phrase * 5
|
||||
|
||||
expected_entities = []
|
||||
phrase_len = len(phrase)
|
||||
for i in range(5):
|
||||
start = i * phrase_len
|
||||
end = start + len(entity_text)
|
||||
ent = Entity(
|
||||
text=entity_text,
|
||||
label="ORG",
|
||||
start_char=start,
|
||||
end_char=end,
|
||||
confidence=0.98,
|
||||
metadata={"lemma": entity_text},
|
||||
)
|
||||
expected_entities.append(ent)
|
||||
|
||||
def custom_ml_extraction(text: str, **method_options):
|
||||
min_confidence = method_options.get("min_confidence", 0.5)
|
||||
entity_types = method_options.get("entity_types")
|
||||
filtered = []
|
||||
for ent in expected_entities:
|
||||
if entity_types and ent.label not in entity_types:
|
||||
continue
|
||||
if ent.confidence >= min_confidence:
|
||||
filtered.append(ent)
|
||||
return filtered
|
||||
|
||||
with patch(
|
||||
"semantica.semantic_extract.methods.get_entity_method"
|
||||
) as mock_get_method:
|
||||
mock_get_method.side_effect = lambda name: (
|
||||
custom_ml_extraction if name == "ml" else (lambda t, **o: [])
|
||||
)
|
||||
|
||||
def op():
|
||||
return extractor.extract_entities(text=medium_text)
|
||||
|
||||
result = benchmark.pedantic(op, rounds=20, iterations=5)
|
||||
|
||||
assert len(result) == 5
|
||||
assert all(e.text == "Semantica" for e in result)
|
||||
assert all(e.label == "ORG" for e in result)
|
||||
assert all(e.confidence == 0.98 for e in result)
|
||||
assert all(medium_text[e.start_char : e.end_char] == e.text for e in result)
|
||||
|
||||
|
||||
# Real spaCy benchmark
|
||||
@pytest.mark.benchmark(group="ner_real_ml")
|
||||
def test_ner_ml_real_performance(benchmark, long_text_string):
|
||||
"""
|
||||
Full spaCy inference + wrapper overhead.
|
||||
Only runs when real spaCy is loaded (BENCHMARK_REAL_LIBS=1).
|
||||
"""
|
||||
extractor = NERExtractor(method="ml", model="en_core_web_sm")
|
||||
|
||||
if (
|
||||
extractor.nlp is None
|
||||
or not hasattr(extractor.nlp, "pipe_names")
|
||||
or "ner" not in extractor.nlp.pipe_names
|
||||
):
|
||||
pytest.skip(
|
||||
"Real spaCy NER pipeline not available — skipping production benchmark"
|
||||
)
|
||||
|
||||
medium_text = long_text_string[:10000]
|
||||
|
||||
medium_text += " Apple Inc. was founded by Steve Jobs and Steve Wozniak in Cupertino, California on April 1, 1976. Microsoft is a competitor."
|
||||
|
||||
def op():
|
||||
return extractor.extract_entities(text=medium_text)
|
||||
|
||||
result = benchmark.pedantic(op, rounds=6, iterations=2)
|
||||
|
||||
assert len(result) >= 6
|
||||
assert any("Apple" in e.text and e.label == "ORG" for e in result)
|
||||
assert any(e.label == "PERSON" for e in result)
|
||||
assert any(e.label in {"GPE", "LOC"} for e in result)
|
||||
assert any(e.label == "DATE" for e in result)
|
||||
assert any("Microsoft" in e.text and e.label == "ORG" for e in result)
|
||||
|
||||
|
||||
def test_ner_pattern_speed(benchmark, long_text_string):
|
||||
extractor = NERExtractor(method="pattern")
|
||||
medium_text = long_text_string[:50000]
|
||||
text_with_entities = medium_text + " Apple Inc. was founded in 1976. "
|
||||
|
||||
def op():
|
||||
return extractor.extract_entities(text=text_with_entities)
|
||||
|
||||
result = benchmark.pedantic(op, rounds=20, iterations=5)
|
||||
assert len(result) > 0
|
||||
assert result[0].label in ["ORG", "DATE", "UNKNOWN"]
|
||||
|
||||
|
||||
def test_ner_batch_throughput(benchmark, document_batch):
|
||||
extractor = NERExtractor(method="pattern")
|
||||
|
||||
def run_batch():
|
||||
return extractor.extract_entities_batch(document_batch, max_workers=2)
|
||||
|
||||
result = benchmark.pedantic(run_batch, rounds=10, iterations=5)
|
||||
assert len(result) == len(document_batch)
|
||||
assert len(result[0]) > 0
|
||||
|
||||
|
||||
def test_similarity_calculation(benchmark):
|
||||
analyzer = SemanticAnalyzer()
|
||||
text1 = "The quick brown fox jumps over the lazy dog" * 10
|
||||
text2 = "The slow brown fox jumped over the sleeping dog" * 10
|
||||
|
||||
def op():
|
||||
return analyzer.calculate_similarity(text1, text2, method="jaccard")
|
||||
|
||||
result = benchmark.pedantic(op, rounds=100, iterations=100)
|
||||
assert 0.0 <= result <= 1.0
|
||||
|
||||
|
||||
def test_clustering_algorithm(benchmark, document_batch):
|
||||
analyzer = SemanticAnalyzer()
|
||||
options = {"similarity_threshold": 0.1}
|
||||
|
||||
def op():
|
||||
return analyzer.cluster_semantically(texts=document_batch, **options)
|
||||
|
||||
result = benchmark.pedantic(op, rounds=10, iterations=5)
|
||||
assert len(result) > 0
|
||||
assert result[0].texts
|
||||
@@ -0,0 +1,56 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
|
||||
|
||||
def test_bulk_node_insertion(benchmark, node_batch):
|
||||
"""
|
||||
Benchmarks the overhead of adding nodes to in-memory graph.
|
||||
|
||||
"""
|
||||
|
||||
def setup_graph():
|
||||
return (ContextGraph(),), {}
|
||||
|
||||
def run(graph_instance):
|
||||
graph_instance.add_nodes(node_batch)
|
||||
|
||||
benchmark.pedantic(target=run, setup=setup_graph, rounds=50, iterations=1)
|
||||
|
||||
|
||||
def test_bulk_edge_insertion(benchmark, node_batch, edge_batch):
|
||||
"""
|
||||
Benchmarks adding edges.
|
||||
"""
|
||||
|
||||
def setup_graph_with_nodes():
|
||||
g = ContextGraph()
|
||||
g.add_nodes(node_batch)
|
||||
return (g,), {}
|
||||
|
||||
def run(graph_instance):
|
||||
graph_instance.add_edges(edge_batch)
|
||||
|
||||
benchmark.pedantic(
|
||||
target=run, setup=setup_graph_with_nodes, rounds=50, iterations=1
|
||||
)
|
||||
|
||||
|
||||
def test_conversation_to_graph_conversion(benchmark, conversation_data):
|
||||
"""
|
||||
Benchmarks parsing conversation dicts into graph structures.
|
||||
"""
|
||||
|
||||
def setup_clean_builder():
|
||||
g = ContextGraph()
|
||||
g.entity_linker = MagicMock()
|
||||
return (g,), {}
|
||||
|
||||
def run(graph_instance):
|
||||
return graph_instance.build_from_conversations(
|
||||
conversation_data, link_entities=False
|
||||
)
|
||||
|
||||
benchmark.pedantic(target=run, setup=setup_clean_builder, rounds=20, iterations=1)
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Mock Arrow Exporter for Benchmark Testing
|
||||
|
||||
This module provides a mock implementation of the ArrowExporter to prevent
|
||||
import errors during benchmark testing when PyArrow is not available in the CI environment.
|
||||
"""
|
||||
|
||||
# Mock PyArrow import for CI compatibility
|
||||
try:
|
||||
import pyarrow as pa
|
||||
except ImportError:
|
||||
# Create a mock pa module for CI environment
|
||||
import types
|
||||
pa = types.ModuleType('pa')
|
||||
|
||||
def mock_schema(*args, **kwargs):
|
||||
return types.SimpleNamespace()
|
||||
|
||||
def mock_table(*args, **kwargs):
|
||||
return types.SimpleNamespace()
|
||||
|
||||
def mock_array(*args, **kwargs):
|
||||
return types.SimpleNamespace()
|
||||
|
||||
pa.schema = mock_schema
|
||||
pa.Table = mock_table
|
||||
pa.array = mock_array
|
||||
pa.RecordBatch = mock_table
|
||||
|
||||
# Mock schema definitions
|
||||
ENTITY_SCHEMA = pa.schema([]) if hasattr(pa, 'schema') else None
|
||||
RELATIONSHIP_SCHEMA = pa.schema([]) if hasattr(pa, 'schema') else None
|
||||
METADATA_SCHEMA = pa.schema([]) if hasattr(pa, 'schema') else None
|
||||
|
||||
class ArrowExporter:
|
||||
"""
|
||||
Mock Arrow Exporter class for benchmark testing.
|
||||
|
||||
This is a lightweight implementation that provides the same interface
|
||||
as the real ArrowExporter but doesn't require PyArrow to be installed.
|
||||
"""
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = config
|
||||
self._tables = {}
|
||||
|
||||
def export_entities(self, entities, output_path):
|
||||
"""Mock export entities method."""
|
||||
return f"Mock exported {len(entities)} entities to {output_path}"
|
||||
|
||||
def export_relationships(self, relationships, output_path):
|
||||
"""Mock export relationships method."""
|
||||
return f"Mock exported {len(relationships)} relationships to {output_path}"
|
||||
|
||||
def export_knowledge_graph(self, entities, relationships, output_path):
|
||||
"""Mock export knowledge graph method."""
|
||||
return f"Mock exported knowledge graph to {output_path}"
|
||||
|
||||
def to_arrow_table(self, data):
|
||||
"""Mock conversion to Arrow table."""
|
||||
return f"Mock Arrow table with {len(data)} rows"
|
||||
|
||||
def save_to_file(self, table, path):
|
||||
"""Mock save to file method."""
|
||||
return f"Mock saved table to {path}"
|
||||
|
||||
def batch_export(self, data_list, output_dir):
|
||||
"""Mock batch export method."""
|
||||
return f"Mock batch exported {len(data_list)} items to {output_dir}"
|
||||
@@ -0,0 +1,81 @@
|
||||
import random
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Data Generators
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_entities():
|
||||
def _gen(count: int) -> List[Dict[str, Any]]:
|
||||
entities = []
|
||||
for i in range(count):
|
||||
entities.append(
|
||||
{
|
||||
"id": f"e_{i}",
|
||||
"text": f"Entity Number {i}",
|
||||
"type": random.choice(
|
||||
["person", "Organization", "Location", "Event"]
|
||||
),
|
||||
"confidence": random.uniform(0.7, 1.0),
|
||||
"metadata": {"source": "doc_1.txt", "page": 1},
|
||||
}
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
return _gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_knowledge_graph(generate_entities):
|
||||
def _gen(entity_count: int, rel_density: float = 1.5) -> Dict[str, Any]:
|
||||
entities = generate_entities(entity_count)
|
||||
relationships = []
|
||||
rel_count = int(entity_count * rel_density)
|
||||
|
||||
for i in range(rel_count):
|
||||
src = random.choice(entities)
|
||||
tgt = random.choice(entities)
|
||||
relationships.append(
|
||||
{
|
||||
"id": f"r_{i}",
|
||||
"source_id": src["id"],
|
||||
"target_id": tgt["id"],
|
||||
"type": " RELATED_TO",
|
||||
"confidence": 0.9,
|
||||
"metadata": {"extractor": "v1"},
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"entities": entities,
|
||||
"relationships": relationships,
|
||||
"metadata": {"generated_at": "2026-02-05"},
|
||||
}
|
||||
|
||||
return _gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_vectors():
|
||||
def _gen(count: int, dim: int = 384) -> List[Dict[str, Any]]:
|
||||
matrix = np.random.rand(count, dim).astype(np.float32)
|
||||
|
||||
data = []
|
||||
|
||||
for i in range(count):
|
||||
data.append(
|
||||
{
|
||||
"id": f"vec_{i}",
|
||||
"vector": matrix[i].tolist(),
|
||||
"text": f"Text {i}",
|
||||
"metadata": {"model": "bert"},
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
return _gen
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
from semantica.export.csv_exporter import CSVExporter
|
||||
from semantica.export.json_exporter import JSONExporter
|
||||
from semantica.export.yaml_exporter import SemanticNetworkYAMLExporter
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="structured_export")
|
||||
@pytest.mark.parametrize("size", [1000, 5000])
|
||||
def test_json_parsing_throughput(benchmark, tmp_path, generate_knowledge_graph, size):
|
||||
kg = generate_knowledge_graph(size)
|
||||
exporter = JSONExporter(indent=None)
|
||||
output_file = tmp_path / "output.json"
|
||||
|
||||
def run():
|
||||
exporter.export(kg, output_file)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="structured_export")
|
||||
def test_csv_entity_export(benchmark, tmp_path, generate_entities):
|
||||
entities = generate_entities(5000)
|
||||
exporter = CSVExporter()
|
||||
output_file = tmp_path / "entities.csv"
|
||||
|
||||
def run():
|
||||
exporter.export_entities(entities, output_file)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="structured_export")
|
||||
def test_yaml_serialization_overhead(benchmark, tmp_path, generate_knowledge_graph):
|
||||
kg = generate_knowledge_graph(500)
|
||||
exporter = SemanticNetworkYAMLExporter()
|
||||
output_file = tmp_path / "output.yaml"
|
||||
|
||||
def run():
|
||||
exporter.export(kg, output_file)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
|
||||
from semantica.export.graph_exporter import GraphExporter
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="vis_export")
|
||||
@pytest.mark.parametrize("format", ["graphml", "gexf"])
|
||||
def test_graph_conversion_overhead(
|
||||
benchmark, tmp_path, generate_knowledge_graph, format
|
||||
):
|
||||
"""
|
||||
Measures the cost of converting internal KG structure to XML-based graph formats.
|
||||
Includes dictionary traversal and XML string building.
|
||||
"""
|
||||
kg = generate_knowledge_graph(2000)
|
||||
exporter = GraphExporter(format=format)
|
||||
output_file = tmp_path / f"graph.{format}"
|
||||
|
||||
def run():
|
||||
exporter.export_knowledge_graph(kg, output_file)
|
||||
|
||||
benchmark(run)
|
||||
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
from semantica.export.lpg_exporter import LPGExporter
|
||||
from semantica.export.owl_exporter import OWLExporter
|
||||
from semantica.export.rdf_exporter import RDFExporter
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="semantic_serialization")
|
||||
@pytest.mark.parametrize("format", ["turtle", "rdfxml"])
|
||||
def test_rdf_serialization_formats(benchmark, generate_knowledge_graph, format):
|
||||
kg = generate_knowledge_graph(1000)
|
||||
exporter = RDFExporter()
|
||||
rdf_data = exporter.serializer.convert_kg_to_rdf(kg)
|
||||
|
||||
def run():
|
||||
return exporter.export_to_rdf(rdf_data, format=format)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="graph_db_export")
|
||||
def test_lpg_cypher_generation(benchmark, generate_knowledge_graph):
|
||||
kg = generate_knowledge_graph(2000)
|
||||
exporter = LPGExporter(batch_size=1000, include_indexes=False)
|
||||
|
||||
def run():
|
||||
return exporter._generate_cypher_queries(kg)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="semantic_serialization")
|
||||
def test_owl_xml_generation(benchmark, tmp_path):
|
||||
ontology = {
|
||||
"name": "BenchmarkOntology",
|
||||
"classes": [{"name": f"Class{i}"} for i in range(500)],
|
||||
"object_properties": [{"name": f"Prop{i}"} for i in range(200)],
|
||||
}
|
||||
exporter = OWLExporter()
|
||||
output_file = tmp_path / "ontology.xml"
|
||||
|
||||
def run():
|
||||
exporter.export(ontology, output_file, format="owl-xml")
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,51 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.export.vector_exporter import VectorExporter
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="vector_io")
|
||||
@pytest.mark.parametrize("count", [1000, 10000])
|
||||
def test_numpy_compression_speed(benchmark, tmp_path, generate_vectors, count):
|
||||
"""
|
||||
Measures cost of np.savez_compressed.
|
||||
"""
|
||||
vectors = generate_vectors(count)
|
||||
exporter = VectorExporter(format="numpy")
|
||||
output_file = tmp_path / "vectors.npz"
|
||||
|
||||
def run():
|
||||
exporter.export(vectors, output_file)
|
||||
|
||||
benchmark(run)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="vector_io")
|
||||
def test_json_vector_overhead(benchmark, tmp_path, generate_vectors):
|
||||
"""
|
||||
Benchmarks JSON export for vectors.
|
||||
"""
|
||||
|
||||
vectors = generate_vectors(2000)
|
||||
exporter = VectorExporter(format="json")
|
||||
output_file = tmp_path / "vectors.json"
|
||||
|
||||
def run():
|
||||
exporter.export(vectors, output_file)
|
||||
|
||||
benchmark(run)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="vector_io")
|
||||
def test_binary_raw_throughput(benchmark, tmp_path, generate_vectors):
|
||||
"""
|
||||
Measures raw binary dump speed (no compression, no metadata).
|
||||
"""
|
||||
vectors = generate_vectors(10000)
|
||||
exporter = VectorExporter(format="binary")
|
||||
output_file = tmp_path / "vectors.bin"
|
||||
|
||||
def run():
|
||||
exporter.export(vectors, output_file)
|
||||
|
||||
benchmark(run)
|
||||
@@ -0,0 +1,102 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def load_results(filepath: str) -> Dict[str, Any]:
|
||||
with open(filepath, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def calc_z_score(current_mean, base_mean, base_stddev):
|
||||
"""
|
||||
Z-Score indicates how many standard deviations
|
||||
away current run is from baseline
|
||||
"""
|
||||
|
||||
if base_stddev == 0:
|
||||
return 0 if current_mean == base_mean else 100.0
|
||||
|
||||
return (current_mean - base_mean) / base_stddev
|
||||
|
||||
|
||||
def compare_benchmarks(
|
||||
baseline: Dict[str, Any], current: Dict[str, Any], threshold_pct: float = 10.0
|
||||
):
|
||||
"""
|
||||
Uses Mean for % change and Z-score for noise detection.
|
||||
"""
|
||||
|
||||
# colors for terminal
|
||||
RED = "\033[91m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
header = f"{'Benchmark':<60} | {'CHANGE %':<12} | {'SIGMA (Z)':<10} | {'STATUS'}"
|
||||
print(header)
|
||||
print("=" * len(header))
|
||||
|
||||
baseline_map = {b["name"]: b for b in baseline["benchmarks"]}
|
||||
current_map = {b["name"]: b for b in current["benchmarks"]}
|
||||
|
||||
regressions = []
|
||||
|
||||
for name, curr in current_map.items():
|
||||
base = baseline_map.get(name)
|
||||
if not base:
|
||||
print(f"{name:<60} | {'NEW':<12} | {'N/A':<10} | NEW")
|
||||
continue
|
||||
|
||||
m1 = base["stats"]["mean"]
|
||||
s1 = base["stats"]["stddev"]
|
||||
m2 = curr["stats"]["mean"]
|
||||
|
||||
if m1 == 0:
|
||||
delta_pct = 0.0
|
||||
else:
|
||||
delta_pct = ((m2 - m1) / m1) * 100
|
||||
|
||||
z_score = calc_z_score(m2, m1, s1)
|
||||
|
||||
status = f"{GREEN} OK{RESET}"
|
||||
|
||||
if delta_pct > threshold_pct:
|
||||
if abs(z_score) > 2.0:
|
||||
status = f"{RED} REGRESSION{RESET}"
|
||||
regressions.append(name)
|
||||
else:
|
||||
status = f"{YELLOW} NOISE{RESET}"
|
||||
elif delta_pct < -threshold_pct and abs(z_score) > 2.0:
|
||||
status = f"{GREEN} IMPROVED{RESET}"
|
||||
|
||||
print(f"{name:<60} | {delta_pct:>+10.2f}% | {z_score:>9.2f} | {status}")
|
||||
|
||||
if regressions:
|
||||
print(
|
||||
f"\n{RED}FAILURE: Performance regression detected in {len(regressions)} tests.{RESET}"
|
||||
)
|
||||
return True
|
||||
print(f"\n{GREEN}SUCCESS: No significant regressions.{RESET}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("baseline", help="Gold standard JSON")
|
||||
parser.add_argument("current", help="NEW RUN JSON")
|
||||
parser.add_argument(
|
||||
"--threshold", type=float, default=10.0, help="FAIL if slower by %"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
failed = compare_benchmarks(
|
||||
load_results(args.baseline), load_results(args.current), args.threshold
|
||||
)
|
||||
sys.exit(1 if failed else 0)
|
||||
except FileNotFoundError as e:
|
||||
print(f"Error loading files: {e}")
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
|
||||
from semantica.ingest.file_ingestor import FileIngestor
|
||||
|
||||
|
||||
def test_ingest_file_performance(benchmark, sample_text_file):
|
||||
"""
|
||||
Benchmarks the speed of the ingest_file method
|
||||
|
||||
Metrics:
|
||||
- Time to open, read, validate and wrap a ~~10 KB text file.
|
||||
"""
|
||||
|
||||
ingestor = FileIngestor()
|
||||
result = benchmark(
|
||||
ingestor.ingest_file, file_path=sample_text_file, read_content=True
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.size > 0
|
||||
assert result.name.endswith(".txt")
|
||||
assert "Line 0" in result.text
|
||||
@@ -0,0 +1,188 @@
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.parse.code_parser import CodeParser
|
||||
from semantica.parse.csv_parser import CSVParser
|
||||
from semantica.parse.document_parser import DocumentParser
|
||||
from semantica.parse.html_parser import HTMLParser
|
||||
from semantica.parse.json_parser import JSONParser
|
||||
|
||||
# Data gens
|
||||
|
||||
|
||||
def generate_json_string(item_count: int) -> str:
|
||||
data = [
|
||||
{
|
||||
"id": i,
|
||||
"name": f"Item:{i}",
|
||||
"tags": ["tag1", "tag2", "tag3"],
|
||||
"metadata": {"active": True, "score": 0.95},
|
||||
}
|
||||
for i in range(item_count)
|
||||
]
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
def generate_csv_string(row_count: int) -> str:
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["id", "name", "description", "value", "date"])
|
||||
for i in range(row_count):
|
||||
writer.writerow([i, f"Item {i}", "Description text here", 100.50, "2024-01-01"])
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def generate_html_string(element_count: int) -> str:
|
||||
lis = "".join(
|
||||
[f'<li><a href="/item/{i}">Link {i}</a></li>' for i in range(element_count)]
|
||||
)
|
||||
return f"""
|
||||
<html>
|
||||
<head><title>Benchmark Page</title></head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1>Header</h1>
|
||||
<p>Some intro text.</p>
|
||||
<ul>{lis}</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
# lib mocks
|
||||
|
||||
|
||||
class MockPDFPage:
|
||||
def __init__(self, page_num):
|
||||
self.width = 600
|
||||
self.height = 800
|
||||
self.page_number = page_num
|
||||
|
||||
def extract_text(self):
|
||||
return f"This is text content for page {self.page_number}. " * 50
|
||||
|
||||
def extract_tables(self):
|
||||
return [[["Header1", "Header2"], ["Row1", "Value1"]]]
|
||||
|
||||
@property
|
||||
def images(self):
|
||||
return [{"x0": 10, "y0": 10, "width": 100, "height": 100}]
|
||||
|
||||
|
||||
class MockPDF:
|
||||
def __init__(self, page_count):
|
||||
self.pages = [MockPDFPage(i) for i in range(page_count)]
|
||||
self.metadata = {"Title": "Benchmark PDF", "Author": "Noone"}
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pdfplumber():
|
||||
with patch("pdfplumber.open") as mock_open:
|
||||
yield mock_open
|
||||
|
||||
|
||||
# Benchmarks
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [1000, 10000])
|
||||
def test_json_parsing_throughput(benchmark, size):
|
||||
parser = JSONParser()
|
||||
json_str = generate_json_string(size)
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
|
||||
def op():
|
||||
return parser.parse(json_str)
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=10)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rows", [1000, 10000])
|
||||
def test_csv_parsing_throughput(benchmark, rows):
|
||||
"""
|
||||
Measures CSV parsing throughput.
|
||||
"""
|
||||
parser = CSVParser()
|
||||
csv_content = generate_csv_string(rows)
|
||||
|
||||
with patch(
|
||||
"builtins.open", side_effect=lambda *args, **kwargs: io.StringIO(csv_content)
|
||||
):
|
||||
with patch("pathlib.Path.exists", return_value=True):
|
||||
|
||||
def op():
|
||||
return parser.parse("dummy.csv")
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("elements", [100, 1000])
|
||||
def test_html_scraping_speed(benchmark, elements):
|
||||
parser = HTMLParser()
|
||||
html_content = generate_html_string(elements)
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
|
||||
def op():
|
||||
return parser.parse(html_content, extract_links=True)
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pages", [10, 50])
|
||||
def test_pdf_extraction_overhead(benchmark, mock_pdfplumber, pages):
|
||||
parser = DocumentParser()
|
||||
|
||||
mock_pdf = MockPDF(pages)
|
||||
mock_pdfplumber.return_value = mock_pdf
|
||||
|
||||
with patch("pathlib.Path.exists", return_value=True), patch(
|
||||
"pathlib.Path.suffix", new_callable=MagicMock(return_value=".pdf")
|
||||
):
|
||||
|
||||
def op():
|
||||
return parser.parse_document("dummy.pdf", extract_images=True)
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
|
||||
|
||||
def test_python_ast_parsing(benchmark):
|
||||
"""
|
||||
Measures performance of Python AST analysis.
|
||||
"""
|
||||
parser = CodeParser()
|
||||
|
||||
code_lines = []
|
||||
for i in range(200):
|
||||
code_lines.append(f"import module_{i}")
|
||||
code_lines.append(f"def function_{i}(arg):")
|
||||
code_lines.append(f" '''Docstring for function {i}'''")
|
||||
code_lines.append(f" return arg + {i}")
|
||||
code_lines.append(f"class Class_{i}:")
|
||||
code_lines.append(f" pass")
|
||||
|
||||
code_content = "\n".join(code_lines)
|
||||
|
||||
with patch(
|
||||
"builtins.open", side_effect=lambda *args, **kwargs: io.StringIO(code_content)
|
||||
), patch("pathlib.Path.exists", return_value=True), patch(
|
||||
"pathlib.Path.suffix", new_callable=MagicMock(return_value=".py")
|
||||
):
|
||||
|
||||
def op():
|
||||
return parser.parse_code("dummy.py")
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
@@ -0,0 +1,27 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
try:
|
||||
from semantica.split.sliding_window_chunker import SlidingWindowChunker
|
||||
from semantica.split.splitter import TextSplitter
|
||||
except ImportError as e:
|
||||
pytest.skip(
|
||||
f"Skipping splitting test due to missing dependencies ({e})",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
def test_sliding_window(benchmark, long_text_string):
|
||||
"""
|
||||
Benchmarks the speed of SlidingWindowChunker in 'Fixed Size' mode
|
||||
"""
|
||||
|
||||
chunker = SlidingWindowChunker(chunk_size=500, overlap=50)
|
||||
|
||||
if hasattr(chunker, "progress_tracker"):
|
||||
chunker.progress_tracker = MagicMock()
|
||||
|
||||
result = benchmark(chunker.chunk, text=long_text_string, preserve_boundaries=False)
|
||||
|
||||
assert len(result) > 0
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Mock Arrow Exporter for Benchmark Testing
|
||||
|
||||
This module provides a mock implementation of the ArrowExporter to prevent
|
||||
import errors during benchmark testing when PyArrow is not available in the CI environment.
|
||||
"""
|
||||
|
||||
# Mock PyArrow import for CI compatibility
|
||||
try:
|
||||
import pyarrow as pa
|
||||
except ImportError:
|
||||
# Create a mock pa module for CI environment
|
||||
import types
|
||||
pa = types.ModuleType('pa')
|
||||
|
||||
def mock_schema(*args, **kwargs):
|
||||
return types.SimpleNamespace()
|
||||
|
||||
def mock_table(*args, **kwargs):
|
||||
return types.SimpleNamespace()
|
||||
|
||||
def mock_array(*args, **kwargs):
|
||||
return types.SimpleNamespace()
|
||||
|
||||
pa.schema = mock_schema
|
||||
pa.Table = mock_table
|
||||
pa.array = mock_array
|
||||
pa.RecordBatch = mock_table
|
||||
|
||||
# Mock schema definitions
|
||||
ENTITY_SCHEMA = pa.schema([]) if hasattr(pa, 'schema') else None
|
||||
RELATIONSHIP_SCHEMA = pa.schema([]) if hasattr(pa, 'schema') else None
|
||||
METADATA_SCHEMA = pa.schema([]) if hasattr(pa, 'schema') else None
|
||||
|
||||
class ArrowExporter:
|
||||
"""
|
||||
Mock Arrow Exporter class for benchmark testing.
|
||||
|
||||
This is a lightweight implementation that provides the same interface
|
||||
as the real ArrowExporter but doesn't require PyArrow to be installed.
|
||||
"""
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = config
|
||||
self._tables = {}
|
||||
|
||||
def export_entities(self, entities, output_path):
|
||||
"""Mock export entities method."""
|
||||
return f"Mock exported {len(entities)} entities to {output_path}"
|
||||
|
||||
def export_relationships(self, relationships, output_path):
|
||||
"""Mock export relationships method."""
|
||||
return f"Mock exported {len(relationships)} relationships to {output_path}"
|
||||
|
||||
def export_knowledge_graph(self, entities, relationships, output_path):
|
||||
"""Mock export knowledge graph method."""
|
||||
return f"Mock exported knowledge graph to {output_path}"
|
||||
|
||||
def to_arrow_table(self, data):
|
||||
"""Mock conversion to Arrow table."""
|
||||
return f"Mock Arrow table with {len(data)} rows"
|
||||
|
||||
def save_to_file(self, table, path):
|
||||
"""Mock save to file method."""
|
||||
return f"Mock saved table to {path}"
|
||||
|
||||
def batch_export(self, data_list, output_dir):
|
||||
"""Mock batch export method."""
|
||||
return f"Mock batch exported {len(data_list)} items to {output_dir}"
|
||||
@@ -0,0 +1,62 @@
|
||||
import random
|
||||
import string
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Data gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_text_data():
|
||||
"""Generates various types of text data."""
|
||||
|
||||
def _gen(type="clean", length=100):
|
||||
if type == "clean":
|
||||
return "".join(random.choices(string.ascii_letters + " ", k=length))
|
||||
elif type == "html":
|
||||
tags = ["<div>", "<p>", "<span>", "<a>", "<b>", "<i>"]
|
||||
content = "".join(random.choices(string.ascii_letters + " ", k=length))
|
||||
return f"{random.choice(tags)}{content}{random.choice(tags).replace('<', '</')}"
|
||||
elif type == "unicode":
|
||||
chars = string.ascii_letters + "éàèùâêîôûçñ"
|
||||
return "".join(random.choices(chars, k=length))
|
||||
elif type == "dirty":
|
||||
chars = string.ascii_letters + " \t\n\r"
|
||||
return "".join(random.choices(chars, k=length))
|
||||
|
||||
return _gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_dataset():
|
||||
"""Generates dataset for data cleaner."""
|
||||
|
||||
def _gen(rows=100, duplicate_rate=0.0):
|
||||
base_rows = []
|
||||
unique_count = int(rows * (1 - duplicate_rate))
|
||||
|
||||
for i in range(unique_count):
|
||||
base_rows.append(
|
||||
{
|
||||
"id": i,
|
||||
"name": f"Entity_{i}",
|
||||
"email": f"user{i}@yahoo.com",
|
||||
"value": random.random() * 100,
|
||||
"category": random.choice(["A", "B", "C"]),
|
||||
}
|
||||
)
|
||||
|
||||
final_dataset = base_rows.copy()
|
||||
while len(final_dataset) < rows:
|
||||
source = random.choice(base_rows)
|
||||
dup = source.copy()
|
||||
if random.random() > 0.5:
|
||||
dup["value"] = source["value"] + 0.001
|
||||
final_dataset.append(dup)
|
||||
|
||||
random.shuffle(final_dataset)
|
||||
return final_dataset
|
||||
|
||||
return _gen
|
||||
@@ -0,0 +1,38 @@
|
||||
import pytest
|
||||
|
||||
from semantica.normalize.data_cleaner import DataCleaner
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rows", [100, 500])
|
||||
def test_duplication_detection_scaling(benchmark, generate_dataset, rows):
|
||||
"""
|
||||
Benchmarks duplicate detection scaling.
|
||||
"""
|
||||
|
||||
cleaner = DataCleaner()
|
||||
dataset = generate_dataset(rows=rows, duplicate_rate=0.2)
|
||||
|
||||
def run():
|
||||
return cleaner.detect_duplicates(dataset, key_fields=["name", "email"])
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
def test_missing_value_imputation(benchmark, generate_dataset):
|
||||
"""
|
||||
Benchmarks statistical imputation.
|
||||
"""
|
||||
cleaner = DataCleaner()
|
||||
|
||||
def setup_broken_dataset():
|
||||
dataset = generate_dataset(rows=5000)
|
||||
for row in dataset:
|
||||
if row["id"] % 5 == 0:
|
||||
row["value"] = None
|
||||
|
||||
return (dataset,), {}
|
||||
|
||||
def run(data):
|
||||
return cleaner.handle_missing_values(data, strategy="impute", method="mean")
|
||||
|
||||
benchmark.pedantic(target=run, setup=setup_broken_dataset, iterations=1, rounds=10)
|
||||
@@ -0,0 +1,31 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.normalize.encoding_handler import EncodingHandler
|
||||
from semantica.normalize.language_detector import LanguageDetector
|
||||
|
||||
|
||||
def test_language_detection_throughput(benchmark, generate_text_data):
|
||||
"""Benchmarks langdetect intergration."""
|
||||
detector = LanguageDetector()
|
||||
texts = [generate_text_data("clean", 200) for _ in range(50)]
|
||||
|
||||
def run():
|
||||
return detector.detect_batch(texts)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
def test_encoding_detection(benchmark):
|
||||
"""Benchmarks chardet integration via EncodingHandler."""
|
||||
handler = EncodingHandler()
|
||||
data = (
|
||||
b"Wowzaaa a simple string for encoding decoding , oh encoding detection just."
|
||||
* 100
|
||||
)
|
||||
|
||||
def run():
|
||||
return handler.detect(data)
|
||||
|
||||
benchmark.pedantic(run, iterations=5, rounds=10)
|
||||
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
|
||||
from semantica.normalize.date_normalizer import DateNormalizer
|
||||
from semantica.normalize.number_normalizer import NumberNormalizer
|
||||
|
||||
|
||||
@pytest.mark.parametrize("date_str", ["2026-02-03", "Ferbuary 2nd, 2026", "9 days ago"])
|
||||
def test_data_parsing_variations(benchmark, date_str):
|
||||
"""Compare speed of different date formats."""
|
||||
normalizer = DateNormalizer()
|
||||
benchmark.pedantic(
|
||||
lambda: normalizer.normalize_date(date_str), iterations=10, rounds=20
|
||||
)
|
||||
|
||||
|
||||
def test_number_normalization(benchmark):
|
||||
"""Benchmarks number parsing with currency and unit stripping."""
|
||||
normalizer = NumberNormalizer()
|
||||
raw_inputs = ["$1,234.56", "1.5k", "50%", "1,000,000"] * 100
|
||||
|
||||
def run():
|
||||
for n in raw_inputs:
|
||||
normalizer.normalize_number(n)
|
||||
|
||||
benchmark.pedantic(run, iterations=5, rounds=20)
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
from semantica.normalize.text_cleaner import TextCleaner
|
||||
from semantica.normalize.text_normalizer import TextNormalizer
|
||||
|
||||
|
||||
def test_html_removal_reg_vs_bs4(benchmark, generate_text_data):
|
||||
"""
|
||||
Compare regex vs BeautifulSoup.
|
||||
"""
|
||||
cleaner = TextCleaner()
|
||||
html_content = generate_text_data("html", 10_000)
|
||||
|
||||
def run():
|
||||
return cleaner.remove_html(html_content, preserve_structure=False)
|
||||
|
||||
benchmark.pedantic(run, rounds=50, iterations=10)
|
||||
|
||||
|
||||
def test_unicode_normalization_throughput(benchmark, generate_text_data):
|
||||
"""
|
||||
Benchmarks unicode NFC normalization speed.
|
||||
"""
|
||||
normalizer = TextNormalizer()
|
||||
text = generate_text_data("unicode", 50_000)
|
||||
|
||||
def run():
|
||||
return normalizer.normalize_text(text, unicode_form="NFC")
|
||||
|
||||
benchmark.pedantic(run, iterations=5, rounds=10)
|
||||
|
||||
|
||||
def test_whitespace_normalization(benchmark, generate_text_data):
|
||||
"""Benchmarks whitespace regex replacement."""
|
||||
normalizer = TextNormalizer()
|
||||
text = generate_text_data("dirty", 50_000)
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: normalizer.normalize_text(text, unicode_form="NFC"),
|
||||
iterations=5,
|
||||
rounds=10,
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
import random
|
||||
import string
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Data generators
|
||||
|
||||
|
||||
def _random_str(length=8):
|
||||
return "".join(random.choices(string.ascii_letters, k=length))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_ontology_data():
|
||||
"""
|
||||
Generates a synthetic dataset of entities and relationships
|
||||
designed to triger class and property inference class.
|
||||
"""
|
||||
|
||||
def _generate(entity_count: int, relationship_density: float = 1.5):
|
||||
|
||||
num_classes = max(5, entity_count // 50)
|
||||
class_names = [f"Class_{_random_str(4)}" for _ in range(num_classes)]
|
||||
|
||||
entities = []
|
||||
|
||||
for i in range(entity_count):
|
||||
cls = random.choice(class_names)
|
||||
|
||||
props = {
|
||||
f"prop_{_random_str(3)}": random.choice([10, "text", 1.5, True])
|
||||
for _ in range(random.randint(1, 5))
|
||||
}
|
||||
|
||||
entity = {
|
||||
"id": f"e_{i}",
|
||||
"type": cls,
|
||||
"name": f"Entity_{i}",
|
||||
"confidence": 0.95,
|
||||
**props,
|
||||
}
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
relationships = []
|
||||
rel_count = int(entity_count * relationship_density)
|
||||
rel_types = ["relatedTo", "hasPart", "worksFor", "contains", "memberOf"]
|
||||
|
||||
for _ in range(rel_count):
|
||||
src = random.choice(entities)
|
||||
tgt = random.choice(entities)
|
||||
rel = {
|
||||
"source": src["name"],
|
||||
"target": tgt["name"],
|
||||
"type": random.choice(rel_types),
|
||||
"source_type": src["type"],
|
||||
"target_type": tgt["type"],
|
||||
"confidence": 0.8,
|
||||
}
|
||||
relationships.append(rel)
|
||||
|
||||
return {"entities": entities, "relationships": relationships}
|
||||
|
||||
return _generate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def large_ontology_definition(generate_ontology_data):
|
||||
"""Pre-calculates a structured ontology
|
||||
definition dictionary.
|
||||
"""
|
||||
from semantica.ontology.ontology_generator import OntologyGenerator
|
||||
|
||||
data = generate_ontology_data(entity_count=1000)
|
||||
|
||||
# Mocking validation in 6-step pipeline to speed up setup
|
||||
|
||||
with patch(
|
||||
"semantica.ontology.ontology_validator.OntologyValidator.validate"
|
||||
) as mock_val:
|
||||
mock_val.return_value.valid = True
|
||||
gen = OntologyGenerator()
|
||||
|
||||
return gen.generate_ontology(data, validate=False)
|
||||
@@ -0,0 +1,70 @@
|
||||
import pytest
|
||||
|
||||
from semantica.ontology.class_inferrer import ClassInferrer
|
||||
from semantica.ontology.property_generator import PropertyGenerator
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="class_Inference")
|
||||
@pytest.mark.parametrize("entity_count", [1000, 5000])
|
||||
def test_class_inference_scaling(benchmark, generate_ontology_data, entity_count):
|
||||
"""
|
||||
Benchmarks grouping and threshold logic in ClassInferrer.
|
||||
"""
|
||||
|
||||
data = generate_ontology_data(entity_count=entity_count)
|
||||
inferrer = ClassInferrer(min_occurrences=2)
|
||||
|
||||
def run():
|
||||
return inferrer.infer_classes(data["entities"])
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="property_inference")
|
||||
@pytest.mark.parametrize("size", [(1000, 1500)])
|
||||
def test_property_inference_scaling(benchmark, generate_ontology_data, size):
|
||||
"""
|
||||
Benchmarks: PropertyGenerator
|
||||
"""
|
||||
|
||||
e_count, _ = size
|
||||
data = generate_ontology_data(entity_count=e_count)
|
||||
|
||||
inferrer = ClassInferrer()
|
||||
classes = inferrer.infer_classes(data["entities"])
|
||||
|
||||
prop_gen = PropertyGenerator()
|
||||
|
||||
def run():
|
||||
return prop_gen.infer_properties(
|
||||
entities=data["entities"],
|
||||
relationships=data["relationships"],
|
||||
classes=classes,
|
||||
)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
def test_hierarchy_circular_detection(benchmark):
|
||||
"""
|
||||
Benchmarks the DFS cycle detection in ClassInferrer.
|
||||
"""
|
||||
|
||||
inferrer = ClassInferrer()
|
||||
|
||||
# Create a deep chain A -> B -> C ... -> Z
|
||||
|
||||
chain_length = 200
|
||||
classes = []
|
||||
|
||||
for i in range(chain_length):
|
||||
cls = {
|
||||
"name": f"Class_{i}",
|
||||
"subClassOf": f"Class_{i+1}" if i < chain_length - 1 else None,
|
||||
}
|
||||
classes.append(cls)
|
||||
|
||||
def run():
|
||||
return inferrer.validate_classes(classes)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=10)
|
||||
@@ -0,0 +1,46 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.ontology.ontology_generator import OntologyGenerator
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="full_pipeline")
|
||||
@pytest.mark.parametrize("entity_count", [1000])
|
||||
def test_e2e_ontology_generation(benchmark, generate_ontology_data, entity_count):
|
||||
"""
|
||||
Benchmarks complete 6-stage pipeline
|
||||
"""
|
||||
|
||||
data = generate_ontology_data(entity_count)
|
||||
generator = OntologyGenerator()
|
||||
|
||||
with patch(
|
||||
"semantica.ontology.ontology_validator.OntologyValidator.validate"
|
||||
) as mock_val:
|
||||
mock_val.return_value.valid = True
|
||||
|
||||
def run():
|
||||
return generator.generate_ontology(data, validate=True)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
def test_associative_class_creation(benchmark):
|
||||
"""
|
||||
Benchmarks the creation of complex N-ary relationships.
|
||||
"""
|
||||
from semantica.ontology.associative_class import AssociativeClassBuilder
|
||||
|
||||
builder = AssociativeClassBuilder()
|
||||
|
||||
def run():
|
||||
for i in range(50):
|
||||
builder.create_position_class(
|
||||
person_class=f"Person_{i}",
|
||||
organization_class=f"Org_{i}",
|
||||
role_class=f"Role_{i}",
|
||||
name=f"Position_{i}",
|
||||
)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=10)
|
||||
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
|
||||
from semantica.ontology.namespace_manager import NamespaceManager
|
||||
from semantica.ontology.reuse_manager import ReuseManager
|
||||
|
||||
|
||||
def test_namespace_iri_generation(benchmark):
|
||||
"""
|
||||
High-throughput test for IRI Generation.
|
||||
"""
|
||||
manager = NamespaceManager(base_uri="https://semantica.dev/bench/")
|
||||
names = [f"EntityName_{i}" for i in range(1000)]
|
||||
|
||||
def run():
|
||||
for name in names:
|
||||
manager.generate_class_iri(name)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=20)
|
||||
|
||||
|
||||
def test_ontology_merging(benchmark, large_ontology_definition):
|
||||
"""
|
||||
Benchmarks merging two large entities together.
|
||||
"""
|
||||
manager = ReuseManager()
|
||||
target = large_ontology_definition.copy()
|
||||
source = large_ontology_definition.copy()
|
||||
|
||||
new_classes = []
|
||||
|
||||
for c in source["classes"]:
|
||||
base_id = c.get("uri") or c.get("name") or "UnkownEntity"
|
||||
new_c = c.copy()
|
||||
new_c["uri"] = f"{base_id}_merged"
|
||||
new_classes.append(new_c)
|
||||
|
||||
source["classes"] = new_classes
|
||||
|
||||
def run():
|
||||
t_copy = target.copy()
|
||||
return manager.merge_ontology_data(t_copy, source, overwrite=False)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=10)
|
||||
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
|
||||
from semantica.ontology.owl_generator import OWLGenerator
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="serialization")
|
||||
@pytest.mark.parametrize("format", ["turtle", "xml"])
|
||||
def test_owl_serialization_formats(benchmark, large_ontology_definition, format):
|
||||
"""Benchmarks the cost of serializing the ontology
|
||||
to different string formats.
|
||||
"""
|
||||
generator = OWLGenerator()
|
||||
|
||||
def run():
|
||||
return generator.generate_owl(large_ontology_definition, format=format)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
def test_rdflib_graph_construction(benchmark, large_ontology_definition):
|
||||
"""
|
||||
Benchmarks the creation of rdflib.Graph object.
|
||||
"""
|
||||
generator = OWLGenerator()
|
||||
|
||||
def run():
|
||||
if hasattr(generator, "_generate_with_rdflib"):
|
||||
return generator._generate_with_rdflib(
|
||||
large_ontology_definition, format="turtle"
|
||||
)
|
||||
return generator.generate_owl(large_ontology_definition)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,98 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.pipeline.execution_engine import ExecutionEngine
|
||||
from semantica.pipeline.pipeline_builder import PipelineBuilder, StepStatus
|
||||
from semantica.pipeline.resource_scheduler import ResourceScheduler
|
||||
|
||||
|
||||
# ~~ Fixtures
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_hardware_checks():
|
||||
with patch.object(ResourceScheduler, "_initialize_resources", return_value=None):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_logging():
|
||||
with patch("semantica.utils.logging.get_logger"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_tracker():
|
||||
mock_tracker = MagicMock()
|
||||
mock_tracker.enabled = False
|
||||
with patch(
|
||||
"semantica.pipeline.execution_engine.get_progress_tracker",
|
||||
return_value=mock_tracker,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def create_pipeline(size):
|
||||
"""Helper to generate pipelines of random size."""
|
||||
builder = PipelineBuilder()
|
||||
builder.progress_tracker = MagicMock()
|
||||
builder.progress_tracker.enabled = False
|
||||
handler = lambda x, **k: x
|
||||
|
||||
builder.add_step("start", "dummy", handler=handler)
|
||||
for i in range(1, size):
|
||||
builder.add_step(f"step_{i}", "dummy", handler=handler)
|
||||
builder.connect_steps("start" if i == 1 else f"step_{i-1}", f"step_{i}")
|
||||
|
||||
return builder.build(f"bench_pipe_{size}")
|
||||
|
||||
|
||||
# ~~ Benchmarks ~~
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step_count", [10, 100, 500])
|
||||
def test_pipeline_construction_scaling(benchmark, step_count):
|
||||
"""
|
||||
Verifies if construction time scales linearly.
|
||||
"""
|
||||
|
||||
def op():
|
||||
builder = PipelineBuilder()
|
||||
builder.progress_tracker = MagicMock()
|
||||
for i in range(step_count):
|
||||
builder.add_step(f"s{i}", "t")
|
||||
return builder.build()
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step_count", [10, 100])
|
||||
def test_execution_overhead_scaling(benchmark, step_count):
|
||||
"""
|
||||
Measures per-step overhead as it gets more complex
|
||||
"""
|
||||
engine = ExecutionEngine()
|
||||
pipeline = create_pipeline(step_count)
|
||||
|
||||
def setup_run():
|
||||
for step in pipeline.steps:
|
||||
step.status = StepStatus.PENDING
|
||||
step.result = None
|
||||
return (pipeline,), {"data": {"val": 1}}
|
||||
|
||||
def op(pipeline, data):
|
||||
return engine.execute_pipeline(pipeline, data=data)
|
||||
|
||||
benchmark.pedantic(op, setup=setup_run, iterations=1, rounds=10)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step_count", [10, 100, 1000])
|
||||
def test_topological_sort_scaling(benchmark, step_count):
|
||||
"""
|
||||
Stress test for dependency graph algorithm.
|
||||
"""
|
||||
engine = ExecutionEngine()
|
||||
pipeline = create_pipeline(step_count)
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: engine._topological_sort(pipeline.steps), iterations=20, rounds=10
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.pipeline.parallelism_manager import ParallelismManager, Task
|
||||
from semantica.pipeline.resource_scheduler import ResourceScheduler
|
||||
|
||||
|
||||
# ~~ Fixtures ~~
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_hardware_checks():
|
||||
with patch.object(ResourceScheduler, "_initialize_resources", return_value=None):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_logging():
|
||||
with patch("semantica.utils.logging.get_logger"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_tracker():
|
||||
mock_tracker = MagicMock()
|
||||
mock_tracker.enabled = False
|
||||
with patch(
|
||||
"semantica.pipeline.parallelism_manager.get_progress_tracker",
|
||||
return_value=mock_tracker,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def blocking_task(duration):
|
||||
"""Simulates a task that waits for I/O (like a DB query or API call)."""
|
||||
time.sleep(duration)
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def thread_manager():
|
||||
return ParallelismManager(max_workers=4, use_processes=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def process_manager():
|
||||
return ParallelismManager(max_workers=4, use_processes=True)
|
||||
|
||||
|
||||
# ~~ BENCHMARKS ~~
|
||||
|
||||
|
||||
def test_parallel_vs_serial_io(benchmark, thread_manager):
|
||||
"""
|
||||
Runs 4 tasks that sleep for 0.1s.
|
||||
"""
|
||||
tasks = [
|
||||
Task(task_id=f"t{i}", handler=blocking_task, args=(0.1,)) for i in range(4)
|
||||
]
|
||||
|
||||
def op():
|
||||
return thread_manager.execute_parallel(tasks)
|
||||
|
||||
benchmark.pedantic(op, iterations=1, rounds=5)
|
||||
|
||||
|
||||
def test_thread_pool_overhead(benchmark, thread_manager):
|
||||
"""
|
||||
Measures the raw cost of spinning up threads for zero-work tasks.
|
||||
"""
|
||||
# No-op handler
|
||||
noop = lambda: None
|
||||
tasks = [Task(task_id=f"t{i}", handler=noop) for i in range(100)]
|
||||
|
||||
def op():
|
||||
return thread_manager.execute_parallel(tasks)
|
||||
|
||||
benchmark.pedantic(op, iterations=5, rounds=10)
|
||||
|
||||
|
||||
def test_process_pool_overhead(benchmark, process_manager):
|
||||
"""
|
||||
Measures overhead of ProcessPoolExecutor
|
||||
"""
|
||||
noop = lambda: None
|
||||
tasks = [Task(task_id=f"t{i}", handler=noop) for i in range(10)]
|
||||
|
||||
def op():
|
||||
return process_manager.execute_parallel(tasks)
|
||||
|
||||
benchmark.pedantic(op, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,84 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.deduplication.merge_strategy import MergeStrategy, MergeStrategyManager
|
||||
|
||||
# Fixtures
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conflict_manager():
|
||||
"""Returns a MergeStrategyManager with default settings."""
|
||||
return MergeStrategyManager()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conflicting_entities_batch():
|
||||
"""
|
||||
Generates a list of 100 entities that are all 'duplicates' of each other
|
||||
but have conflicting property values. This forces the resolution logic to run hard.
|
||||
"""
|
||||
entities = []
|
||||
for i in range(100):
|
||||
entities.append(
|
||||
{
|
||||
"id": "e_1",
|
||||
"name": f"Entity Name {i}",
|
||||
"type": "Person",
|
||||
"confidence": 0.5 + (i * 0.005),
|
||||
"properties": {
|
||||
"age": 20 + i,
|
||||
"email": f"user{i}@example.com",
|
||||
"status": "active" if i % 2 == 0 else "inactive",
|
||||
},
|
||||
"relationships": [
|
||||
{"source": "e_1", "target": f"other_{i}", "type": "knows"}
|
||||
],
|
||||
}
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
# Benchmarks
|
||||
|
||||
|
||||
def test_strategy_keep_highest_confidence(
|
||||
benchmark, conflict_manager, conflicting_entities_batch
|
||||
):
|
||||
"""
|
||||
Benchmarks 'KEEP_HIGHEST_CONFIDENCE'.
|
||||
"""
|
||||
|
||||
def op():
|
||||
return conflict_manager.merge_entities(
|
||||
conflicting_entities_batch, strategy=MergeStrategy.KEEP_HIGHEST_CONFIDENCE
|
||||
)
|
||||
|
||||
benchmark.pedantic(op, iterations=10, rounds=10)
|
||||
|
||||
|
||||
def test_strategy_merge_all(benchmark, conflict_manager, conflicting_entities_batch):
|
||||
"""
|
||||
Benchmarks 'MERGE_ALL'.
|
||||
"""
|
||||
|
||||
def op():
|
||||
return conflict_manager.merge_entities(
|
||||
conflicting_entities_batch, strategy=MergeStrategy.MERGE_ALL
|
||||
)
|
||||
|
||||
benchmark.pedantic(op, iterations=10, rounds=10)
|
||||
|
||||
|
||||
def test_property_resolution_overhead(benchmark, conflict_manager):
|
||||
"""
|
||||
Micro-benchmark for the inner _resolve_property_conflict logic.
|
||||
"""
|
||||
|
||||
def op():
|
||||
return conflict_manager._resolve_property_conflict(
|
||||
"age", 25, 30, MergeStrategy.KEEP_MOST_COMPLETE
|
||||
)
|
||||
|
||||
benchmark.pedantic(op, iterations=1000, rounds=20)
|
||||
@@ -0,0 +1,338 @@
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.deduplication.cluster_builder import ClusterBuilder
|
||||
from semantica.deduplication.duplicate_detector import DuplicateDetector
|
||||
from semantica.deduplication.entity_merger import EntityMerger
|
||||
from semantica.deduplication.similarity_calculator import SimilarityCalculator
|
||||
|
||||
# Infra
|
||||
|
||||
|
||||
class NullTracker:
|
||||
"""
|
||||
Discards all data to prevent memory leaks
|
||||
"""
|
||||
|
||||
def start_tracking(self, *args, **kwargs):
|
||||
return "dummy_id"
|
||||
|
||||
def update_tracking(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def stop_tracking(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def register_pipeline_modules(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def clear_pipeline_context(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def update_progress(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return False
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_io_overhead():
|
||||
"""
|
||||
Replaces ProgressTracker with NullTracker globally.
|
||||
"""
|
||||
with patch("semantica.utils.logging.get_logger"), patch(
|
||||
"semantica.utils.progress_tracker.get_progress_tracker"
|
||||
) as mock_getter:
|
||||
|
||||
mock_getter.return_value = NullTracker()
|
||||
|
||||
with patch(
|
||||
"semantica.deduplication.similarity_calculator.get_progress_tracker",
|
||||
return_value=NullTracker(),
|
||||
), patch(
|
||||
"semantica.deduplication.duplicate_detector.get_progress_tracker",
|
||||
return_value=NullTracker(),
|
||||
), patch(
|
||||
"semantica.deduplication.cluster_builder.get_progress_tracker",
|
||||
return_value=NullTracker(),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
# Sim data
|
||||
|
||||
|
||||
def generate_entity_cluster(base_name: str, size: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generates a cluster of similar entities based on a seed name.
|
||||
Example: "Apple" -> ["Apple Inc", "Apple Corp", etc.]
|
||||
"""
|
||||
|
||||
entities = []
|
||||
suffixes = ["Inc", "Corp", "Ltd", "Gmbh", "LLC", "Group", "Systems"]
|
||||
|
||||
for i in range(size):
|
||||
if random.random() < 0.8:
|
||||
name = f"{base_name} {random.choice(suffixes)}"
|
||||
else:
|
||||
# Generating a typo for our calc to work on
|
||||
chars = list(base_name)
|
||||
if len(chars) > 2:
|
||||
idx = random.randint(0, len(chars) - 2)
|
||||
chars[idx], chars[idx + 1] = chars[idx + 1], chars[idx]
|
||||
name = "".join(chars)
|
||||
|
||||
entities.append(
|
||||
{
|
||||
"id": f"{base_name.lower()}_{i}",
|
||||
"name": name,
|
||||
"type": "Organization",
|
||||
"properties": {
|
||||
"location": "USA" if i % 2 == 0 else "California",
|
||||
"sector": "Tech",
|
||||
"employee_count": 100 + i,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
):
|
||||
"""
|
||||
Generates a full dataset
|
||||
|
||||
Args:
|
||||
worst_case_blocking: If True, all names start with 'A' to defeat
|
||||
first-char blocking strategy in SimilarityCalculator.
|
||||
|
||||
"""
|
||||
dataset = []
|
||||
for i in range(num_clusters):
|
||||
if worst_case_blocking:
|
||||
# All starts with 'A'
|
||||
base_name = f"A_Company_{i}"
|
||||
else:
|
||||
start_char = random.choice(string.ascii_uppercase)
|
||||
base_name = f"{start_char}_company_{i}"
|
||||
|
||||
cluster = generate_entity_cluster(base_name, items_per_cluster)
|
||||
dataset.extend(cluster)
|
||||
|
||||
return dataset
|
||||
|
||||
|
||||
# ~~ Benchmarks ~~
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["levenshtein", "jaro_winkler"])
|
||||
def test_string_metric_speed(benchmark, method):
|
||||
"""
|
||||
Measures the speed of string comparison algos.
|
||||
"""
|
||||
|
||||
calc = SimilarityCalculator()
|
||||
s1 = "International Business Machines Corporation"
|
||||
s2 = "International Business Machine Corp."
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: calc.calculate_string_similarity(s1, s2, method=method),
|
||||
iterations=1000,
|
||||
rounds=100,
|
||||
)
|
||||
|
||||
|
||||
def test_full_similarity_calculation(benchmark):
|
||||
"""
|
||||
Measures weighted multi-factor calculation overhead.
|
||||
(String + Property + Relationship + Weights).
|
||||
"""
|
||||
|
||||
calc = SimilarityCalculator(
|
||||
string_weight=0.5, property_weight=0.3, relationship_weight=0.2
|
||||
)
|
||||
|
||||
e1 = {
|
||||
"name": "Acme Corp",
|
||||
"properties": {"loc": "NY", "id": "123"},
|
||||
"relationships": [{"target": "t1"}, {"target": "t2"}],
|
||||
}
|
||||
|
||||
e2 = {
|
||||
"name": "Acme Inc",
|
||||
"properties": {"loc": "NY", "id": "123"},
|
||||
"relationships": [{"target": "t1"}, {"target": "t2"}],
|
||||
}
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: calc.calculate_similarity(e1, e2), iterations=1000, rounds=50
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dataset_size", [100, 500])
|
||||
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,
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dataset_size", [100, 500])
|
||||
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,
|
||||
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)
|
||||
|
||||
|
||||
def test_incremental_detection_speed(benchmark):
|
||||
"""
|
||||
Measures performance of adding new data to existing index.
|
||||
"""
|
||||
|
||||
existing = generate_dataset(num_clusters=50, items_per_cluster=5)
|
||||
new_data = generate_dataset(num_clusters=5, items_per_cluster=2)
|
||||
|
||||
detector = DuplicateDetector()
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: detector.incremental_detect(new_data, existing), iterations=5, rounds=10
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("algo", ["graph", "hierarchical"])
|
||||
def test_clustering_strategy_performance(benchmark, algo):
|
||||
"""
|
||||
Comapres Union-Fund (Graph) vs Hierarchical Clustering.
|
||||
"""
|
||||
|
||||
data = generate_dataset(num_clusters=20, items_per_cluster=10)
|
||||
|
||||
use_hierarchical = algo == "hierarchical"
|
||||
builder = ClusterBuilder(use_hierarchical=use_hierarchical)
|
||||
|
||||
benchmark.pedantic(lambda: builder.build_clusters(data), iterations=1, rounds=5)
|
||||
|
||||
|
||||
def test_merge_entity_benchmark(benchmark):
|
||||
"""
|
||||
Measures the cost of fusing entities / res conflicts.
|
||||
"""
|
||||
|
||||
group = generate_entity_cluster("MegaCorp", 50)
|
||||
merger = EntityMerger()
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: merger.merge_entity_group(group, strategy="keep_most_complete"),
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Benchmark Tools
|
||||
|
||||
pytest>=7.0.0
|
||||
pytest-benchmark>=4.0.0
|
||||
|
||||
# Core Utils
|
||||
|
||||
pydantic
|
||||
loguru
|
||||
chardet
|
||||
requests
|
||||
greenlet
|
||||
typing-extensions
|
||||
tqdm
|
||||
click
|
||||
rich
|
||||
|
||||
numpy
|
||||
pandas
|
||||
networkx
|
||||
scikit-learn
|
||||
|
||||
# Graph & Storage
|
||||
|
||||
sqlalchemy
|
||||
rdflib
|
||||
neo4j
|
||||
redis
|
||||
|
||||
# AI proc
|
||||
|
||||
torch
|
||||
transformers
|
||||
sentence-transformers
|
||||
spacy
|
||||
beautifulsoup4
|
||||
lxml
|
||||
pypdf2
|
||||
python-docx
|
||||
openpyxl
|
||||
pillow
|
||||
feedparser
|
||||
GitPython
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
from typing import Generator, List
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.embeddings.embedding_generator import EmbeddingGenerator
|
||||
from semantica.embeddings.graph_embedding_manager import GraphEmbeddingManager
|
||||
from semantica.embeddings.pooling_strategies import PoolingStrategyFactory
|
||||
from semantica.embeddings.text_embedder import TextEmbedder
|
||||
|
||||
|
||||
# Infra Mocks
|
||||
@pytest.fixture(autouse=True)
|
||||
def kill_io_overhead():
|
||||
"""Silences logging and tracker globally."""
|
||||
with patch("semantica.utils.logging.get_logger"), patch(
|
||||
"semantica.utils.progress_tracker.get_progress_tracker"
|
||||
) as mock_tracker:
|
||||
|
||||
tracker = MagicMock()
|
||||
tracker.enabled = False
|
||||
tracker._start_tracking.return_value = "dummy_id"
|
||||
mock_tracker.return_value = tracker
|
||||
|
||||
with patch(
|
||||
"semantica.embeddings.text_embedder.get_progress_tracker",
|
||||
return_value=tracker,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
# __ Model Mocks __
|
||||
|
||||
|
||||
class MockSentenceTransformer:
|
||||
"""
|
||||
Simulates ST.encode without loading the fat model itself.
|
||||
"""
|
||||
|
||||
def __init__(self, dim=384):
|
||||
self.dim = dim
|
||||
|
||||
def encode(
|
||||
self, sentences: List[str], normalize_embeddings=True, **kwargs
|
||||
) -> np.ndarray:
|
||||
count = len(sentences)
|
||||
return np.random.rand(count, self.dim).astype(np.float32)
|
||||
|
||||
def get_sentence_embedding_dimension(self):
|
||||
return self.dim
|
||||
|
||||
|
||||
class MockFastEmbed:
|
||||
"""
|
||||
Simulates FastEmbed.embed generator behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, dim=384):
|
||||
self.dim = dim
|
||||
|
||||
def embed(self, documents: List[str]) -> Generator[np.ndarray, None, None]:
|
||||
for _ in documents:
|
||||
yield np.random.rand(self.dim).astype(np.float32)
|
||||
|
||||
|
||||
# ~~ Fixtures ~~
|
||||
@pytest.fixture
|
||||
def text_embedder_st():
|
||||
"""
|
||||
Text embedder configured with SentenceTransformer
|
||||
"""
|
||||
embedder = TextEmbedder(method="sentence_transformers", model_name="mock-bert")
|
||||
embedder.model = MockSentenceTransformer()
|
||||
embedder.progress_tracker = MagicMock()
|
||||
embedder.progress_tracker.enabled = False
|
||||
|
||||
return embedder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def text_embedder_fast():
|
||||
"""
|
||||
Text Embedder cofnigures with Mock FastEmbed.
|
||||
"""
|
||||
|
||||
embedder = TextEmbedder(method="fastembed", model_name="mock-bge")
|
||||
embedder.fastembed_model = MockFastEmbed()
|
||||
embedder.progress_tracker = MagicMock()
|
||||
embedder.progress_tracker.enabled = False
|
||||
return embedder
|
||||
|
||||
|
||||
# ~~ Benchmarks
|
||||
|
||||
|
||||
@pytest.mark.parametrize("strategy", ["mean", "max", "cls", "attention"])
|
||||
def test_pooling_math_speed(benchmark, strategy):
|
||||
"""
|
||||
Measures the raw NumPy speed of pooling strategies.
|
||||
Scenario: Pooling a batch of 128 token embeddings.
|
||||
"""
|
||||
|
||||
embeddings = np.random.rand(128, 768).astype(np.float32)
|
||||
pooler = PoolingStrategyFactory.create(strategy)
|
||||
|
||||
benchmark.pedantic(lambda: pooler.pool(embeddings), iterations=1000, rounds=100)
|
||||
|
||||
|
||||
def test_hierarchical_pooling_overhead(benchmark):
|
||||
"""
|
||||
Measures the overhead of two-step hierarchical pooling.
|
||||
"""
|
||||
|
||||
embeddings = np.random.rand(1000, 768).astype(np.float32)
|
||||
pooler = PoolingStrategyFactory.create("hierarchical", chunk_size=100)
|
||||
|
||||
benchmark.pedantic(lambda: pooler.pool(embeddings), iterations=500, rounds=50)
|
||||
|
||||
|
||||
def test_st_wrapper_overhead(benchmark, text_embedder_st):
|
||||
"""
|
||||
Measures overhead of TextEmbedder wrapper around SentenceTransformers.
|
||||
"""
|
||||
|
||||
text = "This is a whatever we are doing here since idk"
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: text_embedder_st.embed_text(text), iterations=1000, rounds=20
|
||||
)
|
||||
|
||||
|
||||
def test_fastembed_generator_consumption(benchmark, text_embedder_fast):
|
||||
"""
|
||||
Measures the cost of consuming the FastEmbed generator
|
||||
and converting to Array.
|
||||
"""
|
||||
texts = [f"Sentence {i}" for i in range(20)]
|
||||
|
||||
benchmark.pedantic(
|
||||
lambda: text_embedder_fast.embed_batch(texts), iterations=100, rounds=20
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [10, 100, 1000])
|
||||
def test_batch_processing_pipeline(benchmark, batch_size, text_embedder_st):
|
||||
"""
|
||||
Measures the full EmbeddingGenerator pipeline:
|
||||
Input validation -> Type detection -> Batching -> Mock Model -> Error handling.
|
||||
"""
|
||||
|
||||
generator = EmbeddingGenerator()
|
||||
|
||||
generator.text_embedder = text_embedder_st
|
||||
generator.progress_tracker = MagicMock()
|
||||
generator.progress_tracker.enabled = False
|
||||
|
||||
data = [f"Item {i}" for i in range(batch_size)]
|
||||
|
||||
benchmark.pedantic(lambda: generator.process_batch(data), iterations=5, rounds=10)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count", [100, 1000])
|
||||
def test_graph_embedding_prep(benchmark, count, text_embedder_st):
|
||||
"""
|
||||
Measures how fast we can reshape dict for GraphDBs
|
||||
"""
|
||||
manager = GraphEmbeddingManager()
|
||||
manager.embedding_generator.text_embedder = text_embedder_st
|
||||
|
||||
manager.embedding_generator.generate_embeddings = MagicMock(
|
||||
return_value=np.random.rand(count, 384).astype(np.float32)
|
||||
)
|
||||
|
||||
entities = [{"id": f"e{i}", "text": f"Entity{i}"} for i in range(count)]
|
||||
|
||||
def op():
|
||||
return manager.prepare_for_graph_db(entities, backend="neo4j")
|
||||
|
||||
benchmark.pedantic(op, iterations=10, rounds=10)
|
||||
@@ -0,0 +1,137 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.graph_store.graph_store import GraphStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_neo4j_driver():
|
||||
"""
|
||||
Creates a mock of of Neo4j Driver
|
||||
Simulates: Driver -> Session -> Transaction -> Result -> Record
|
||||
"""
|
||||
|
||||
mock_result = MagicMock()
|
||||
fake_props = {"name": "TestNode", "age": 30}
|
||||
|
||||
def get_item(key):
|
||||
if key == "id":
|
||||
return 12345
|
||||
if key == "n":
|
||||
return fake_props
|
||||
if key == "count":
|
||||
return 42
|
||||
return None
|
||||
|
||||
mock_record = MagicMock()
|
||||
mock_record.__getitem__.side_effect = get_item
|
||||
mock_record.keys.return_value = ["id", "n"]
|
||||
mock_record.values.return_value = [12345, fake_props]
|
||||
|
||||
# dict conversion - essentially doing it because the db sometimes demands it
|
||||
mock_record.items.return_value = [("id", 12345), ("n", fake_props)]
|
||||
|
||||
# ~~ Result Methods ~~
|
||||
mock_result = MagicMock()
|
||||
mock_result.single.return_value = mock_record
|
||||
mock_result.__iter__.side_effect = lambda: iter([mock_record])
|
||||
|
||||
# ~~ Session ~~
|
||||
mock_session = MagicMock()
|
||||
mock_session.run.return_value = mock_result
|
||||
mock_session.__enter__.return_value = mock_session
|
||||
mock_session.__exit__.return_value = None
|
||||
|
||||
# ~~ Driver ~~
|
||||
mock_driver = MagicMock()
|
||||
mock_driver.session.return_value = mock_session
|
||||
mock_driver.verify_connectivity.return_value = True
|
||||
|
||||
return mock_driver
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def graph_store(mock_neo4j_driver):
|
||||
"""
|
||||
Returns a GraphsStore connected to mnock driver.
|
||||
"""
|
||||
|
||||
# ~~ Patch GraphDatbase ~~
|
||||
with patch("semantica.graph_store.neo4j_store.GraphDatabase") as mockDB:
|
||||
mockDB.driver.return_value = mock_neo4j_driver
|
||||
store = GraphStore(
|
||||
backend="neo4j", uri="bolt://mock:7687", user="mock", password="mock"
|
||||
)
|
||||
store.connect()
|
||||
|
||||
if hasattr(store, "progress_tracker"):
|
||||
store.progress_tracker = MagicMock()
|
||||
|
||||
return store
|
||||
|
||||
|
||||
# ~~ Benchmarks ~~
|
||||
|
||||
|
||||
def test_node_creation_overhead(benchmark, graph_store):
|
||||
"""
|
||||
Benchamrks the full stack overhead for creating a single node.
|
||||
Path: GraphStore -> NodeManager -> Neo4jStore, Driver
|
||||
"""
|
||||
|
||||
def op():
|
||||
return graph_store.create_node(
|
||||
labels=["Person"], properties={"name": "Alexander", "age": 17}
|
||||
)
|
||||
|
||||
result = benchmark(op)
|
||||
assert result["id"] == 12345
|
||||
|
||||
|
||||
def test_batch_node_creation_overhead(benchmark, graph_store):
|
||||
"""
|
||||
Benchmarks the loop overhead in create_nodes (Batch).
|
||||
Checks if it handles lists efficiently.
|
||||
"""
|
||||
|
||||
nodes = [{"labels": ["Person"], "properties": {"id": i}} for i in range(50)]
|
||||
|
||||
def op():
|
||||
return graph_store.create_nodes(nodes)
|
||||
|
||||
result = benchmark(op)
|
||||
assert len(result) == 50
|
||||
|
||||
|
||||
def test_query_construction_and_parsing(benchmark, graph_store):
|
||||
"""
|
||||
Benchmarks every execution overhead.
|
||||
Measures how fast `QueryEngine` parses result into a Python dict.
|
||||
"""
|
||||
|
||||
query = "MATCH ( n:Person) RETURN n LIMIT 1"
|
||||
|
||||
def op():
|
||||
return graph_store.execute_query(query)
|
||||
|
||||
result = benchmark(op)
|
||||
assert result["success"] is True
|
||||
assert len(result["records"]) > 0
|
||||
|
||||
|
||||
def test_analytics_shortest_path_overhead(benchmark, graph_store):
|
||||
"""
|
||||
Benchmarks the wrapper overhead for graph analytics.
|
||||
"""
|
||||
|
||||
def op():
|
||||
return graph_store.shortest_path(
|
||||
start_node_id=1, end_node_id=2, rel_type="KNOWS"
|
||||
)
|
||||
|
||||
try:
|
||||
benchmark(op)
|
||||
except Exception:
|
||||
# v pass as we are only trying to benchmark the function overhead call mainly
|
||||
pass
|
||||
@@ -0,0 +1,146 @@
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.triplet_store.bulk_loader import BulkLoader
|
||||
from semantica.triplet_store.jena_store import JenaStore
|
||||
from semantica.triplet_store.triplet_store import TripletStore
|
||||
|
||||
# ~~ Mocking ~~
|
||||
# We basically define a facile Triplet class for creating ds devoid of fat AI models
|
||||
|
||||
|
||||
@dataclass
|
||||
class SimpleTriplet:
|
||||
subject: str
|
||||
predicate: str
|
||||
object: str
|
||||
confidence: float = 1.0
|
||||
|
||||
|
||||
# ~~ Fixtures ~~
|
||||
@pytest.fixture
|
||||
def triplet_batch():
|
||||
"""Generates 1000 triplets."""
|
||||
return [
|
||||
SimpleTriplet(
|
||||
subject=f"http://gandhara.org/entity/{i}",
|
||||
predicate="http://gandhara.org/relation/knows",
|
||||
object=f"http://example.org/entity/{i+1}",
|
||||
)
|
||||
for i in range(1000)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def large_knowledge_graph_dict():
|
||||
"""
|
||||
Generates a large dict (1000 ent) to test parsing
|
||||
logic in `TripletStore.store()`
|
||||
"""
|
||||
entities = [
|
||||
{
|
||||
"id": f"ent_{i}",
|
||||
"type": "Person",
|
||||
"properties": {"name": f"Person {i}", "age": 60},
|
||||
}
|
||||
for i in range(1000)
|
||||
]
|
||||
relationships = [
|
||||
{"source": f"ent_{i}", "target": f"ent_{i+1}", "type": "KNOWS"}
|
||||
for i in range(999)
|
||||
]
|
||||
|
||||
return {"entities": entities, "relationships": relationships}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def in_memory_store():
|
||||
"""Returns a real JenaStore using RDFLib (In-Mmeory)."""
|
||||
|
||||
store = JenaStore(endpoint=None)
|
||||
if store.graph is None:
|
||||
pytest.fail("JenaStore failed to initialize rdflib graph.")
|
||||
if hasattr(store, "progress_tracker"):
|
||||
store.progress_tracker = MagicMock()
|
||||
|
||||
return store
|
||||
|
||||
|
||||
# ~~ Benchmarks ~~
|
||||
|
||||
|
||||
def test_rdflib_insert_throughput(benchmark, in_memory_store, triplet_batch):
|
||||
"""
|
||||
Benchmarks raw Write Speed to in-memory RDF graph.
|
||||
Is our baseline
|
||||
"""
|
||||
|
||||
def op():
|
||||
in_memory_store.add_triplets(triplet_batch)
|
||||
|
||||
benchmark(op)
|
||||
|
||||
assert len(in_memory_store.graph) >= 1000
|
||||
|
||||
|
||||
def test_triplet_conversion_overhead(benchmark, large_knowledge_graph_dict):
|
||||
"""
|
||||
Benchmarks the `store()` method in TripletStore.
|
||||
This tests Python logic that converts a Dict -> Triplet objects.
|
||||
"""
|
||||
|
||||
with patch("semantica.triplet_store.blazegraph_store.BlazegraphStore") as mockBE:
|
||||
mock_instance = mockBE.return_value
|
||||
mock_instance.add_triplets.return_value = {"success": True}
|
||||
|
||||
manager = TripletStore(backend="blazegraph")
|
||||
if hasattr(manager, "progress_tracker"):
|
||||
manager.progress_tracker = MagicMock()
|
||||
|
||||
def op():
|
||||
manager.store(
|
||||
knowledge_graph=large_knowledge_graph_dict,
|
||||
ontology={"classes": [], "properties": []},
|
||||
)
|
||||
|
||||
benchmark(op)
|
||||
|
||||
|
||||
def test_bulk_loader_logic(benchmark, triplet_batch):
|
||||
"""
|
||||
Benchmarks teh BulkLoader class.
|
||||
Measures the overhead of batching, retries and progress tracking.
|
||||
"""
|
||||
|
||||
loader = BulkLoader(batch_size=100)
|
||||
if hasattr(loader, "progress_tracker"):
|
||||
loader.progress_tracker = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_store.add_triplets.return_value = {"success": True}
|
||||
|
||||
def op():
|
||||
return loader.load_triplets(triplet_batch, mock_store)
|
||||
|
||||
result = benchmark(op)
|
||||
assert result.total_batches == 10
|
||||
|
||||
|
||||
def test_sparql_query_performance(benchmark, in_memory_store, triplet_batch):
|
||||
"""
|
||||
Benchamrks SPARQL query execution speed on 1000 items.
|
||||
"""
|
||||
|
||||
in_memory_store.add_triplets(triplet_batch)
|
||||
|
||||
query = "SELECT ?s ?o WHERE { ?s <http://gandhara.org/relation/knows> ?o } LIMIT 50"
|
||||
|
||||
def op():
|
||||
return in_memory_store.execute_sparql(query)
|
||||
|
||||
result = benchmark(op)
|
||||
assert result["success"] is True
|
||||
assert len(result["bindings"]) == 50
|
||||
@@ -0,0 +1,94 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.vector_store.faiss_store import FAISSStore
|
||||
from semantica.vector_store.vector_store import VectorStore
|
||||
|
||||
# Fixtures
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vector_dim():
|
||||
return 768
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_vectors(vector_dim):
|
||||
"""Generates a batch of 10,000 rando vectors."""
|
||||
count = 10000
|
||||
vectors = np.random.rand(count, vector_dim).astype(np.float32)
|
||||
return vectors
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def populated_store(random_vectors, vector_dim):
|
||||
"""
|
||||
Returns a FAISS store bred with data.
|
||||
"""
|
||||
|
||||
store = FAISSStore(dimension=vector_dim)
|
||||
if hasattr(store, "progress_tracker"):
|
||||
store.progress_tracker = MagicMock()
|
||||
store.create_index(index_type="flat")
|
||||
store.add_vectors(random_vectors)
|
||||
return store
|
||||
|
||||
|
||||
# Benchmarks
|
||||
|
||||
|
||||
def test_faiss_insert_throughput(benchmark, random_vectors, vector_dim):
|
||||
"""
|
||||
Benchmarks raw Write speed to FAISS
|
||||
"""
|
||||
store = FAISSStore(dimension=vector_dim)
|
||||
if hasattr(store, "progress_tracker"):
|
||||
store.progress_tracker = MagicMock()
|
||||
store.create_index(index_type="flat")
|
||||
|
||||
def insert_op():
|
||||
store.add_vectors(random_vectors)
|
||||
|
||||
benchmark(insert_op)
|
||||
|
||||
assert len(store.index.vector_ids) >= 10000
|
||||
|
||||
|
||||
def test_faiss_search_latency(benchmark, populated_store, vector_dim):
|
||||
"""
|
||||
Benchmarks Read/Search speed
|
||||
"""
|
||||
|
||||
query = np.random.rand(1, vector_dim).astype(np.float32)
|
||||
results = benchmark(populated_store.search_similar, query_vector=query, k=10)
|
||||
assert len(results) == 10
|
||||
|
||||
|
||||
def test_vector_storage_manager_overhead(benchmark, random_vectors, vector_dim):
|
||||
"""
|
||||
Benchmarks the overhead of the VectorStore class
|
||||
"""
|
||||
with patch(
|
||||
"semantica.vector_store.vector_store.EmbeddingGenerator"
|
||||
) as MockEmbedder:
|
||||
manager = VectorStore(backend="faiss", dimension=vector_dim)
|
||||
if hasattr(manager, "progress_tracker"):
|
||||
manager.progress_tracker = MagicMock()
|
||||
|
||||
def store_op():
|
||||
manager.store_vectors(random_vectors)
|
||||
|
||||
benchmark(store_op)
|
||||
|
||||
# Check vectors were stored - handle both in-memory and backend stores
|
||||
if hasattr(manager, 'vectors'):
|
||||
# In-memory backend
|
||||
assert len(manager.vectors) >= 10000
|
||||
elif hasattr(manager, '_backend_store') and hasattr(manager._backend_store, 'vector_ids'):
|
||||
# Backend store (like FAISS)
|
||||
assert len(manager._backend_store.vector_ids) >= 10000
|
||||
else:
|
||||
# For other backends, just ensure no errors occurred
|
||||
pass
|
||||
@@ -0,0 +1,80 @@
|
||||
import random
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
# Data Generators
|
||||
@pytest.fixture
|
||||
def generate_embeddings():
|
||||
"""Generates synthetic high-dim embeddings."""
|
||||
|
||||
def _gen(n_samples: int, n_features: int = 768):
|
||||
return np.random.rand(n_samples, n_features).astype(np.float32)
|
||||
|
||||
return _gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_knowledge_graph():
|
||||
"""Generates synthetic Knowledge Graph dictionary."""
|
||||
|
||||
def _gen(n_nodes: int, density: float = 0.05):
|
||||
entities = [
|
||||
{
|
||||
"id": f"e_{i}",
|
||||
"label": f"Entity_{i}",
|
||||
"type": random.choice(["Person", "Organization", "Location", "Event"]),
|
||||
"metadata": {"score": random.random()},
|
||||
}
|
||||
for i in range(n_nodes)
|
||||
]
|
||||
|
||||
relationships = []
|
||||
n_edges = int(n_nodes * (n_nodes - 1) * density)
|
||||
# Capping edges for safety
|
||||
n_edges = min(n_edges, n_nodes * 5)
|
||||
|
||||
for i in range(n_edges):
|
||||
src = random.randint(0, n_nodes - 1)
|
||||
tgt = random.randint(0, n_nodes - 1)
|
||||
|
||||
if src != tgt:
|
||||
relationships.append(
|
||||
{
|
||||
"source": f"e_{src}",
|
||||
"target": f"e_{tgt}",
|
||||
"type": "related_to",
|
||||
"metadata": {"weight": random.random()},
|
||||
}
|
||||
)
|
||||
|
||||
return {"entities": entities, "relationships": relationships}
|
||||
|
||||
return _gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_temporal_data(generate_knowledge_graph):
|
||||
"""Generates synthetic temporal graph snapshots."""
|
||||
|
||||
def _gen(n_snapshots: int, n_nodes: int):
|
||||
timestamps_map = {}
|
||||
base_kg = generate_knowledge_graph(n_nodes)
|
||||
entities = base_kg["entities"]
|
||||
|
||||
all_years = list(range(2020, 2020 + n_snapshots))
|
||||
for ent in entities:
|
||||
start = random.randint(0, len(all_years) - 2)
|
||||
duration = random.randint(1, len(all_years) - start)
|
||||
timestamps_map[ent["id"]] = all_years[start : start + duration]
|
||||
|
||||
return {
|
||||
"entities": entities,
|
||||
"relationships": base_kg["relationships"],
|
||||
"timestamps": timestamps_map,
|
||||
}
|
||||
|
||||
return _gen
|
||||
@@ -0,0 +1,26 @@
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.visualization.analytics_visualizer import AnalyticsVisualizer
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="analytics_charts")
|
||||
def test_centrality_ranking_sort_and_render(benchmark):
|
||||
"""
|
||||
Benchmarks sorting a large centrality dictionary
|
||||
and rendering the Top N bar chart.
|
||||
"""
|
||||
viz = AnalyticsVisualizer()
|
||||
|
||||
# Generate 5000 node scores
|
||||
centrality_data = {
|
||||
"centrality": {f"node_{i}": random.random() for i in range(5000)}
|
||||
}
|
||||
|
||||
def run():
|
||||
return viz.visualize_centrality_rankings(
|
||||
centrality_data, centrality_type="degree", top_n=50, output="interactive"
|
||||
)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=10)
|
||||
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.visualization.embedding_visualizer import EmbeddingVisualizer
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="embedding_projection")
|
||||
@pytest.mark.parametrize("method", ["pca", "tsne"])
|
||||
@pytest.mark.parametrize("n_samples", [500])
|
||||
def test_projection_calculation_overhead(
|
||||
benchmark, generate_embeddings, method, n_samples
|
||||
):
|
||||
"""
|
||||
Measures the combined cost of:
|
||||
1. Dimensionality Reduction (Math)
|
||||
2. Plotly Trace Construction (Object creation)
|
||||
"""
|
||||
|
||||
viz = EmbeddingVisualizer()
|
||||
embeddings = generate_embeddings(n_samples=n_samples, n_features=128)
|
||||
labels = [f"Label {i}" for i in range(n_samples)]
|
||||
|
||||
def run():
|
||||
return viz.visualize_2d_projection(
|
||||
embeddings, labels=labels, method=method, output="interactive"
|
||||
)
|
||||
|
||||
rounds = 5 if method == "tsne" else 10
|
||||
benchmark.pedantic(run, iterations=1, rounds=rounds)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="embedding_heatmap")
|
||||
def test_similarity_heatmap_generation(benchmark, generate_embeddings):
|
||||
"""
|
||||
Benchmarks O(N^2) similarity matrix calculation
|
||||
and heatmap renderin.
|
||||
"""
|
||||
|
||||
viz = EmbeddingVisualizer()
|
||||
embeddings = generate_embeddings(n_samples=500, n_features=64)
|
||||
|
||||
def run():
|
||||
return viz.visualize_similarity_heatmap(embeddings, output="interactive")
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
|
||||
from semantica.visualization.kg_visualizer import KGVisualizer
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="graph_layouyt")
|
||||
@pytest.mark.parametrize("layout", ["circular", "force"])
|
||||
@pytest.mark.parametrize("size", [100])
|
||||
def test_network_layout_performance(benchmark, generate_knowledge_graph, layout, size):
|
||||
"""
|
||||
Compares layout algorithm.
|
||||
"""
|
||||
viz = KGVisualizer(layout=layout, force_layout_iterations=50)
|
||||
graph = generate_knowledge_graph(n_nodes=size)
|
||||
|
||||
def run():
|
||||
return viz.visualize_network(graph, output="interactive")
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="graph_structure")
|
||||
def test_matrix_view_rendering(benchmark, generate_knowledge_graph):
|
||||
"""
|
||||
Benchmarks the creation of an adjacent/relationship matrix.
|
||||
"""
|
||||
viz = KGVisualizer()
|
||||
graph = generate_knowledge_graph(n_nodes=500)
|
||||
|
||||
def run():
|
||||
return viz.visualize_relationship_matrix(graph, output="interactive")
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
|
||||
from semantica.visualization.temporal_visualizer import TemporalVisualizer
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="temporal_animation")
|
||||
def test_network_evolution_frames(benchmark, generate_temporal_data):
|
||||
"""
|
||||
Measures the cost of generating animation frames for Plotly.
|
||||
"""
|
||||
|
||||
temporal_data = generate_temporal_data(n_snapshots=5, n_nodes=100)
|
||||
viz = TemporalVisualizer()
|
||||
|
||||
def run():
|
||||
return viz.visualize_network_evolution(temporal_data, output="interactive")
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group="temporal_dashboard")
|
||||
def test_temporal_dashboard_assembly(benchmark, generate_temporal_data):
|
||||
"""
|
||||
Benchmarks the creation of a multi-subplot dashboard.
|
||||
"""
|
||||
temporal_data = generate_temporal_data(n_snapshots=20, n_nodes=200)
|
||||
viz = TemporalVisualizer()
|
||||
|
||||
metrics = {
|
||||
"Accuracy": [0.5 + i * 0.02 for i in range(20)],
|
||||
"Loss": [1.0 - i * 0.04 for i in range(20)],
|
||||
}
|
||||
|
||||
def run():
|
||||
return viz.visualize_temporal_dashboard(
|
||||
temporal_data, metrics=metrics, output="interactive"
|
||||
)
|
||||
|
||||
benchmark.pedantic(run, iterations=1, rounds=5)
|
||||
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
Snowflake Ingestion Examples
|
||||
|
||||
This module provides comprehensive examples of using the Snowflake ingestor.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from semantica.ingest import SnowflakeIngestor
|
||||
from semantica.utils.logging import get_logger
|
||||
|
||||
logger = get_logger("snowflake_examples")
|
||||
|
||||
|
||||
def example_basic_ingestion():
|
||||
"""Example: Basic table ingestion."""
|
||||
print("\n=== Example 1: Basic Table Ingestion ===\n")
|
||||
|
||||
# Initialize ingestor with password authentication
|
||||
ingestor = SnowflakeIngestor(
|
||||
account=os.getenv("SNOWFLAKE_ACCOUNT"),
|
||||
user=os.getenv("SNOWFLAKE_USER"),
|
||||
password=os.getenv("SNOWFLAKE_PASSWORD"),
|
||||
warehouse="COMPUTE_WH",
|
||||
database="SAMPLE_DB",
|
||||
schema="PUBLIC",
|
||||
)
|
||||
|
||||
# Ingest a table
|
||||
data = ingestor.ingest_table("CUSTOMERS", limit=10)
|
||||
|
||||
print(f"Retrieved {data.row_count} rows")
|
||||
print(f"Columns: {data.columns}")
|
||||
print(f"\nFirst row:")
|
||||
print(data.data[0])
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_query_execution():
|
||||
"""Example: Execute custom SQL queries."""
|
||||
print("\n=== Example 2: Query Execution ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# Execute aggregation query
|
||||
query = """
|
||||
SELECT
|
||||
COUNTRY,
|
||||
COUNT(*) AS CUSTOMER_COUNT,
|
||||
SUM(TOTAL_PURCHASES) AS TOTAL_REVENUE
|
||||
FROM CUSTOMERS
|
||||
GROUP BY COUNTRY
|
||||
ORDER BY TOTAL_REVENUE DESC
|
||||
LIMIT 10
|
||||
"""
|
||||
|
||||
data = ingestor.ingest_query(query)
|
||||
|
||||
print(f"Top 10 countries by revenue:")
|
||||
for row in data.data:
|
||||
print(
|
||||
f" {row['COUNTRY']}: {row['CUSTOMER_COUNT']} customers, "
|
||||
f"${row['TOTAL_REVENUE']:,.2f} revenue"
|
||||
)
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_parameterized_query():
|
||||
"""Example: Parameterized queries."""
|
||||
print("\n=== Example 3: Parameterized Queries ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# Calculate date range
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
# Execute parameterized query
|
||||
query = """
|
||||
SELECT
|
||||
ORDER_ID,
|
||||
CUSTOMER_ID,
|
||||
PRODUCT_NAME,
|
||||
AMOUNT,
|
||||
ORDER_DATE
|
||||
FROM ORDERS
|
||||
WHERE ORDER_DATE BETWEEN %(start_date)s AND %(end_date)s
|
||||
AND AMOUNT > %(min_amount)s
|
||||
ORDER BY ORDER_DATE DESC
|
||||
"""
|
||||
|
||||
data = ingestor.ingest_query(
|
||||
query,
|
||||
params={
|
||||
"start_date": start_date.strftime("%Y-%m-%d"),
|
||||
"end_date": end_date.strftime("%Y-%m-%d"),
|
||||
"min_amount": 100.0,
|
||||
},
|
||||
)
|
||||
|
||||
print(f"Found {data.row_count} orders in the last 30 days over $100")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_schema_introspection():
|
||||
"""Example: Table schema introspection."""
|
||||
print("\n=== Example 4: Schema Introspection ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# Get table schema
|
||||
schema = ingestor.get_table_schema("CUSTOMERS")
|
||||
|
||||
print("Table schema for CUSTOMERS:")
|
||||
print(f"Primary keys: {schema['primary_keys']}\n")
|
||||
|
||||
print("Columns:")
|
||||
for col in schema["columns"]:
|
||||
nullable = "NULL" if col["nullable"] else "NOT NULL"
|
||||
default = f" DEFAULT {col['default']}" if col["default"] else ""
|
||||
print(f" {col['name']}: {col['type']} {nullable}{default}")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_list_tables():
|
||||
"""Example: List all tables in a schema."""
|
||||
print("\n=== Example 5: List Tables ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# List tables in current schema
|
||||
tables = ingestor.list_tables()
|
||||
|
||||
print(f"Found {len(tables)} tables:")
|
||||
for table in tables:
|
||||
print(f" - {table}")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_pagination():
|
||||
"""Example: Paginate large result sets."""
|
||||
print("\n=== Example 6: Pagination ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
PAGE_SIZE = 100
|
||||
total_rows = 0
|
||||
|
||||
# Paginate through large table
|
||||
page = 0
|
||||
while True:
|
||||
data = ingestor.ingest_table(
|
||||
"LARGE_TABLE", limit=PAGE_SIZE, offset=page * PAGE_SIZE
|
||||
)
|
||||
|
||||
if data.row_count == 0:
|
||||
break
|
||||
|
||||
total_rows += data.row_count
|
||||
print(f"Page {page + 1}: {data.row_count} rows")
|
||||
|
||||
# Process page
|
||||
process_page(data)
|
||||
|
||||
page += 1
|
||||
|
||||
print(f"\nTotal rows processed: {total_rows}")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_batch_processing():
|
||||
"""Example: Batch processing with fetchmany."""
|
||||
print("\n=== Example 7: Batch Processing ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# Execute query with batching
|
||||
data = ingestor.ingest_query(
|
||||
"SELECT * FROM LARGE_TABLE WHERE STATUS = 'ACTIVE'", batch_size=1000
|
||||
)
|
||||
|
||||
print(f"Retrieved {data.row_count} rows in batches of 1000")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_export_documents():
|
||||
"""Example: Export to Semantica document format."""
|
||||
print("\n=== Example 8: Export as Documents ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# Ingest product data
|
||||
data = ingestor.ingest_table("PRODUCTS", limit=10)
|
||||
|
||||
# Convert to documents
|
||||
documents = ingestor.export_as_documents(
|
||||
data, id_field="PRODUCT_ID", text_fields=["PRODUCT_NAME", "DESCRIPTION"]
|
||||
)
|
||||
|
||||
print(f"Exported {len(documents)} documents")
|
||||
print("\nFirst document:")
|
||||
print(f" ID: {documents[0]['id']}")
|
||||
print(f" Text: {documents[0]['text'][:100]}...")
|
||||
print(f" Metadata: {documents[0]['metadata']}")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_key_pair_auth():
|
||||
"""Example: Key-pair authentication."""
|
||||
print("\n=== Example 9: Key-Pair Authentication ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor(
|
||||
account=os.getenv("SNOWFLAKE_ACCOUNT"),
|
||||
user=os.getenv("SNOWFLAKE_USER"),
|
||||
private_key_path=os.getenv("SNOWFLAKE_PRIVATE_KEY_PATH"),
|
||||
warehouse="COMPUTE_WH",
|
||||
)
|
||||
|
||||
data = ingestor.ingest_table("CUSTOMERS", limit=5)
|
||||
print(f"Successfully authenticated and retrieved {data.row_count} rows")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_context_manager():
|
||||
"""Example: Using context manager."""
|
||||
print("\n=== Example 10: Context Manager ===\n")
|
||||
|
||||
with SnowflakeIngestor() as ingestor:
|
||||
data = ingestor.ingest_table("CUSTOMERS", limit=5)
|
||||
print(f"Retrieved {data.row_count} rows")
|
||||
|
||||
# Connection automatically closed
|
||||
print("Connection closed automatically")
|
||||
|
||||
|
||||
def example_multi_schema():
|
||||
"""Example: Multi-schema ingestion."""
|
||||
print("\n=== Example 11: Multi-Schema Ingestion ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# Ingest from different schemas
|
||||
prod_customers = ingestor.ingest_table(
|
||||
"CUSTOMERS", database="PROD_DB", schema="PUBLIC", limit=10
|
||||
)
|
||||
|
||||
staging_customers = ingestor.ingest_table(
|
||||
"CUSTOMERS", database="STAGING_DB", schema="PUBLIC", limit=10
|
||||
)
|
||||
|
||||
print(f"Production customers: {prod_customers.row_count}")
|
||||
print(f"Staging customers: {staging_customers.row_count}")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_error_handling():
|
||||
"""Example: Error handling."""
|
||||
print("\n=== Example 12: Error Handling ===\n")
|
||||
|
||||
from semantica.utils.exceptions import ProcessingError, ValidationError
|
||||
|
||||
try:
|
||||
# Try to connect with invalid credentials
|
||||
ingestor = SnowflakeIngestor(
|
||||
account="invalid_account", user="invalid_user", password="invalid_password"
|
||||
)
|
||||
|
||||
data = ingestor.ingest_table("CUSTOMERS")
|
||||
|
||||
except ValidationError as e:
|
||||
print(f"Validation error: {e}")
|
||||
|
||||
except ProcessingError as e:
|
||||
print(f"Processing error: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
|
||||
|
||||
def example_incremental_load():
|
||||
"""Example: Incremental data loading."""
|
||||
print("\n=== Example 13: Incremental Loading ===\n")
|
||||
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
# Get last load timestamp (from your metadata store)
|
||||
last_load = get_last_load_timestamp() # Your function
|
||||
|
||||
# Query only new/updated records
|
||||
query = """
|
||||
SELECT *
|
||||
FROM CUSTOMERS
|
||||
WHERE UPDATED_AT > %(last_load)s
|
||||
ORDER BY UPDATED_AT ASC
|
||||
"""
|
||||
|
||||
data = ingestor.ingest_query(query, params={"last_load": last_load})
|
||||
|
||||
print(f"Loaded {data.row_count} new/updated records since {last_load}")
|
||||
|
||||
# Update last load timestamp
|
||||
if data.row_count > 0:
|
||||
update_last_load_timestamp(datetime.now())
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
def example_etl_pipeline():
|
||||
"""Example: Full ETL pipeline."""
|
||||
print("\n=== Example 14: ETL Pipeline ===\n")
|
||||
|
||||
# Extract
|
||||
ingestor = SnowflakeIngestor()
|
||||
|
||||
sales_query = """
|
||||
SELECT
|
||||
s.ORDER_ID,
|
||||
s.CUSTOMER_ID,
|
||||
c.CUSTOMER_NAME,
|
||||
s.PRODUCT_ID,
|
||||
p.PRODUCT_NAME,
|
||||
s.AMOUNT,
|
||||
s.ORDER_DATE
|
||||
FROM SALES s
|
||||
JOIN CUSTOMERS c ON s.CUSTOMER_ID = c.ID
|
||||
JOIN PRODUCTS p ON s.PRODUCT_ID = p.ID
|
||||
WHERE s.ORDER_DATE >= CURRENT_DATE - 7
|
||||
"""
|
||||
|
||||
data = ingestor.ingest_query(sales_query)
|
||||
print(f"Extracted {data.row_count} sales records")
|
||||
|
||||
# Transform
|
||||
documents = ingestor.export_as_documents(
|
||||
data, id_field="ORDER_ID", text_fields=["CUSTOMER_NAME", "PRODUCT_NAME"]
|
||||
)
|
||||
print(f"Transformed to {len(documents)} documents")
|
||||
|
||||
# Load (into Semantica)
|
||||
from semantica.pipeline import Pipeline
|
||||
|
||||
pipeline = Pipeline()
|
||||
|
||||
for doc in documents:
|
||||
pipeline.process_document(doc)
|
||||
|
||||
print("Loaded documents into Semantica pipeline")
|
||||
|
||||
ingestor.close()
|
||||
|
||||
|
||||
# Utility functions for examples
|
||||
def process_page(data):
|
||||
"""Process a page of data."""
|
||||
# Your processing logic here
|
||||
pass
|
||||
|
||||
|
||||
def get_last_load_timestamp():
|
||||
"""Get the last load timestamp from metadata store."""
|
||||
# Your implementation here
|
||||
return (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def update_last_load_timestamp(timestamp):
|
||||
"""Update the last load timestamp in metadata store."""
|
||||
# Your implementation here
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all examples."""
|
||||
examples = [
|
||||
example_basic_ingestion,
|
||||
example_query_execution,
|
||||
example_parameterized_query,
|
||||
example_schema_introspection,
|
||||
example_list_tables,
|
||||
example_export_documents,
|
||||
example_context_manager,
|
||||
example_error_handling,
|
||||
]
|
||||
|
||||
for example_func in examples:
|
||||
try:
|
||||
example_func()
|
||||
except Exception as e:
|
||||
logger.error(f"Example {example_func.__name__} failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Set up environment variables
|
||||
# export SNOWFLAKE_ACCOUNT=your_account
|
||||
# export SNOWFLAKE_USER=your_user
|
||||
# export SNOWFLAKE_PASSWORD=your_password
|
||||
# export SNOWFLAKE_WAREHOUSE=COMPUTE_WH
|
||||
# export SNOWFLAKE_DATABASE=SAMPLE_DB
|
||||
# export SNOWFLAKE_SCHEMA=PUBLIC
|
||||
|
||||
main()
|
||||
@@ -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/semantica)** - 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/semantica)
|
||||
[Get Started Now](https://semantica.readthedocs.io/quickstart/) • [View Examples](https://github.com/semantica/examples) • [Join Community](https://discord.gg/sV34vps5hH)
|
||||
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ semantica/
|
||||
│ │ └── custom.css # Custom styling
|
||||
│ └── assets/
|
||||
│ └── img/
|
||||
│ └── Semantica Updated Logo.png
|
||||
│ └── Semantica Logo.png
|
||||
└── site/ # Generated site (created by mkdocs build)
|
||||
```
|
||||
|
||||
|
||||
+3
-3
@@ -1380,11 +1380,11 @@ result = semantica.build_knowledge_base(["document.pdf"])
|
||||
## 🚀 Performance
|
||||
|
||||
### Benchmarks
|
||||
- **Processing Speed**: 1000+ documents per minute
|
||||
- **Processing Speed**: Optimized for high-throughput document processing
|
||||
- **Memory Usage**: Optimized for large-scale processing
|
||||
- **Accuracy**: 95%+ entity extraction accuracy
|
||||
- **Accuracy**: High accuracy entity extraction
|
||||
- **Scalability**: Horizontal scaling support
|
||||
- **Latency**: Sub-second query response times
|
||||
- **Latency**: Fast query response times
|
||||
|
||||
### Optimization
|
||||
- **Parallel Processing**: Multi-threaded and multi-process support
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# Apache Arrow Exporter
|
||||
|
||||
## Overview
|
||||
|
||||
The Apache Arrow exporter provides high-performance columnar data export for Semantica's knowledge graphs, entities, and relationships. It uses explicit schemas (no inference) and writes Arrow IPC files (.arrow) that are compatible with Pandas and DuckDB.
|
||||
|
||||
## Features
|
||||
|
||||
- **Explicit Schemas**: Pre-defined schemas for entities and relationships (no inference)
|
||||
- **Columnar Format**: Efficient storage and fast analytics
|
||||
- **Metadata Support**: Converts metadata dictionaries to Arrow struct fields
|
||||
- **Field Normalization**: Handles various entity and relationship field name variations
|
||||
- **Progress Tracking**: Integrated progress monitoring
|
||||
- **Error Handling**: Structured error handling with detailed logging
|
||||
- **Pandas/DuckDB Compatible**: Direct conversion to DataFrames and SQL queries
|
||||
|
||||
## Installation
|
||||
|
||||
The Arrow exporter requires PyArrow:
|
||||
|
||||
```bash
|
||||
pip install pyarrow
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from semantica.export import ArrowExporter
|
||||
|
||||
# Initialize exporter
|
||||
exporter = ArrowExporter()
|
||||
|
||||
# Export entities
|
||||
entities = [
|
||||
{"id": "e1", "text": "Alice", "type": "Person", "confidence": 0.95},
|
||||
{"id": "e2", "text": "Acme Corp", "type": "Organization", "confidence": 0.88}
|
||||
]
|
||||
exporter.export_entities(entities, "entities.arrow")
|
||||
|
||||
# Export relationships
|
||||
relationships = [
|
||||
{"id": "r1", "source_id": "e1", "target_id": "e2", "type": "WORKS_FOR"}
|
||||
]
|
||||
exporter.export_relationships(relationships, "relationships.arrow")
|
||||
|
||||
# Export knowledge graph
|
||||
knowledge_graph = {
|
||||
"entities": entities,
|
||||
"relationships": relationships
|
||||
}
|
||||
exporter.export_knowledge_graph(knowledge_graph, "kg_base")
|
||||
# Creates: kg_base_entities.arrow, kg_base_relationships.arrow
|
||||
```
|
||||
|
||||
### Using Convenience Function
|
||||
|
||||
```python
|
||||
from semantica.export import export_arrow
|
||||
|
||||
# Simple export
|
||||
export_arrow(entities, "entities.arrow")
|
||||
|
||||
# Export multiple types
|
||||
data = {
|
||||
"entities": entities,
|
||||
"relationships": relationships
|
||||
}
|
||||
export_arrow(data, "output_base")
|
||||
```
|
||||
|
||||
### With Compression
|
||||
|
||||
```python
|
||||
# Use LZ4 compression
|
||||
exporter = ArrowExporter(compression="lz4")
|
||||
exporter.export_entities(entities, "entities_compressed.arrow")
|
||||
```
|
||||
|
||||
## Schemas
|
||||
|
||||
### Entity Schema
|
||||
|
||||
```python
|
||||
ENTITY_SCHEMA = pa.schema([
|
||||
pa.field("id", pa.string(), nullable=False),
|
||||
pa.field("text", pa.string(), nullable=True),
|
||||
pa.field("type", pa.string(), nullable=True),
|
||||
pa.field("confidence", pa.float64(), nullable=True),
|
||||
pa.field("start", pa.int64(), nullable=True),
|
||||
pa.field("end", pa.int64(), nullable=True),
|
||||
pa.field("metadata", pa.struct([
|
||||
pa.field("keys", pa.list_(pa.string())),
|
||||
pa.field("values", pa.list_(pa.string()))
|
||||
]), nullable=True),
|
||||
])
|
||||
```
|
||||
|
||||
### Relationship Schema
|
||||
|
||||
```python
|
||||
RELATIONSHIP_SCHEMA = pa.schema([
|
||||
pa.field("id", pa.string(), nullable=False),
|
||||
pa.field("source_id", pa.string(), nullable=False),
|
||||
pa.field("target_id", pa.string(), nullable=False),
|
||||
pa.field("type", pa.string(), nullable=True),
|
||||
pa.field("confidence", pa.float64(), nullable=True),
|
||||
pa.field("metadata", pa.struct([
|
||||
pa.field("keys", pa.list_(pa.string())),
|
||||
pa.field("values", pa.list_(pa.string()))
|
||||
]), nullable=True),
|
||||
])
|
||||
```
|
||||
|
||||
## Field Normalization
|
||||
|
||||
The exporter automatically normalizes field names:
|
||||
|
||||
**Entities:**
|
||||
- `text`, `label`, `name` → `text`
|
||||
- `type`, `entity_type` → `type`
|
||||
- `id`, `entity_id` → `id`
|
||||
- `start`, `start_offset` → `start`
|
||||
- `end`, `end_offset` → `end`
|
||||
|
||||
**Relationships:**
|
||||
- `source`, `source_id` → `source_id`
|
||||
- `target`, `target_id` → `target_id`
|
||||
- `type`, `relationship_type` → `type`
|
||||
|
||||
## Reading Arrow Files
|
||||
|
||||
### With PyArrow
|
||||
|
||||
```python
|
||||
import pyarrow as pa
|
||||
import pyarrow.ipc as ipc
|
||||
|
||||
with pa.OSFile("entities.arrow", 'rb') as source:
|
||||
with ipc.open_file(source) as reader:
|
||||
table = reader.read_all()
|
||||
print(table.schema)
|
||||
print(table.to_pandas())
|
||||
```
|
||||
|
||||
### With Pandas
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import pyarrow.ipc as ipc
|
||||
|
||||
with ipc.open_file("entities.arrow") as reader:
|
||||
df = reader.read_all().to_pandas()
|
||||
print(df)
|
||||
```
|
||||
|
||||
### With DuckDB
|
||||
|
||||
```python
|
||||
import duckdb
|
||||
|
||||
# Query Arrow file directly
|
||||
result = duckdb.query("SELECT * FROM 'entities.arrow' WHERE type = 'Person'")
|
||||
print(result.df())
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
### `export(data, file_path, schema=None, **options)`
|
||||
|
||||
Generic export method that handles both single and multiple files.
|
||||
|
||||
**Parameters:**
|
||||
- `data`: List of dicts or dict with list values
|
||||
- `file_path`: Output file path (base path for dict exports)
|
||||
- `schema`: Optional Arrow schema (auto-detected if not provided)
|
||||
- `**options`: Additional options
|
||||
|
||||
### `export_entities(entities, file_path, **options)`
|
||||
|
||||
Export entities to Arrow IPC file with normalization.
|
||||
|
||||
**Parameters:**
|
||||
- `entities`: List of entity dictionaries
|
||||
- `file_path`: Output Arrow file path
|
||||
- `**options`: Additional options
|
||||
|
||||
### `export_relationships(relationships, file_path, **options)`
|
||||
|
||||
Export relationships to Arrow IPC file with normalization.
|
||||
|
||||
**Parameters:**
|
||||
- `relationships`: List of relationship dictionaries
|
||||
- `file_path`: Output Arrow file path
|
||||
- `**options`: Additional options
|
||||
|
||||
### `export_knowledge_graph(knowledge_graph, base_path, **options)`
|
||||
|
||||
Export knowledge graph to multiple Arrow files.
|
||||
|
||||
**Parameters:**
|
||||
- `knowledge_graph`: Knowledge graph dictionary with 'entities' and 'relationships'
|
||||
- `base_path`: Base path for output files (without extension)
|
||||
- `**options`: Additional options
|
||||
|
||||
## Examples
|
||||
|
||||
See `examples/arrow_export_example.py` for comprehensive usage examples.
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# All Arrow exporter tests
|
||||
pytest tests/test_arrow_exporter.py -v
|
||||
|
||||
# Integration tests
|
||||
pytest tests/test_export_module.py::TestExportModule::test_arrow_exporter -v
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
- **Columnar Storage**: Faster analytics on specific columns
|
||||
- **Compression**: Smaller file sizes (especially with LZ4/ZSTD)
|
||||
- **Zero-Copy**: Memory-efficient data transfer
|
||||
- **Cross-Language**: Works with Python, R, Julia, JavaScript, and more
|
||||
- **SQL Queries**: Direct querying with DuckDB without loading into memory
|
||||
|
||||
## Comparison with Other Formats
|
||||
|
||||
| Feature | Arrow | CSV | JSON |
|
||||
|---------|-------|-----|------|
|
||||
| Type Safety | ✓ | ✗ | ✗ |
|
||||
| Compression | ✓ | ✗ | ✗ |
|
||||
| Schema Validation | ✓ | ✗ | ✗ |
|
||||
| Pandas Compatible | ✓ | ✓ | ✓ |
|
||||
| DuckDB Native | ✓ | ✓ | ✗ |
|
||||
| Binary Format | ✓ | ✗ | ✗ |
|
||||
| Human Readable | ✗ | ✓ | ✓ |
|
||||
|
||||
## Architecture
|
||||
|
||||
The Arrow exporter follows Semantica's export architecture:
|
||||
|
||||
1. **Normalization**: Field names are normalized to consistent format
|
||||
2. **Schema Application**: Explicit schemas ensure type safety
|
||||
3. **Metadata Conversion**: Dicts converted to Arrow struct fields
|
||||
4. **Progress Tracking**: Integrated with Semantica's progress tracker
|
||||
5. **Error Handling**: Structured exceptions with detailed messages
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to the Arrow exporter:
|
||||
|
||||
1. Maintain explicit schemas (no inference)
|
||||
2. Follow existing code style and patterns
|
||||
3. Add comprehensive tests for new features
|
||||
4. Update this documentation
|
||||
5. Ensure Pandas/DuckDB compatibility
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details.
|
||||
|
||||
## Author
|
||||
|
||||
Semantica Contributors
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 494 KiB |
@@ -1,3 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
--8<-- "CHANGELOG.md"
|
||||
+5
-5
@@ -12,22 +12,22 @@ How to cite Semantica in academic papers and research.
|
||||
author = {Hawksight AI},
|
||||
year = {2026},
|
||||
url = {https://github.com/Hawksight-AI/semantica},
|
||||
version = {0.2.5},
|
||||
version = {0.2.7},
|
||||
doi = {10.5281/zenodo.XXXXXXX}
|
||||
}
|
||||
```
|
||||
|
||||
### APA
|
||||
Hawksight AI. (2026). *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering* (Version 0.2.5) [Computer software]. https://github.com/Hawksight-AI/semantica
|
||||
Hawksight AI. (2026). *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering* (Version 0.2.7) [Computer software]. https://github.com/Hawksight-AI/semantica
|
||||
|
||||
### MLA
|
||||
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.5, GitHub, 2026, https://github.com/Hawksight-AI/semantica.
|
||||
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.7, GitHub, 2026, https://github.com/Hawksight-AI/semantica.
|
||||
|
||||
### Chicago
|
||||
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.5. GitHub, 2026. https://github.com/Hawksight-AI/semantica.
|
||||
Hawksight AI. *Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering*. Version 0.2.7. GitHub, 2026. https://github.com/Hawksight-AI/semantica.
|
||||
|
||||
### IEEE
|
||||
Hawksight AI, "Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering," Version 0.2.5, GitHub, 2026. [Online]. Available: https://github.com/Hawksight-AI/semantica
|
||||
Hawksight AI, "Semantica: An Open Source Framework for Semantic Layers and Knowledge Engineering," Version 0.2.7, GitHub, 2026. [Online]. Available: https://github.com/Hawksight-AI/semantica
|
||||
|
||||
---
|
||||
|
||||
|
||||
+43
-56
@@ -1,86 +1,73 @@
|
||||
# Community
|
||||
# Community
|
||||
|
||||
Welcome to the Semantica community!
|
||||
|
||||
!!! info "Join Us"
|
||||
We're building an open, collaborative community around semantic AI and knowledge graphs.
|
||||
**Connect with the Semantica community for support, collaboration, and learning.**
|
||||
|
||||
---
|
||||
|
||||
## 💬 Communication Channels
|
||||
## Get Help & Support
|
||||
|
||||
### GitHub
|
||||
|
||||
- **[Issues](https://github.com/Hawksight-AI/semantica/issues)** - Bug reports, feature requests, questions
|
||||
### GitHub Issues
|
||||
- **[Report Issues](https://github.com/Hawksight-AI/semantica/issues)** - Bug reports and feature requests
|
||||
- **[Pull Requests](https://github.com/Hawksight-AI/semantica/pulls)** - Code contributions
|
||||
- **[Releases](https://github.com/Hawksight-AI/semantica/releases)** - Release announcements
|
||||
- **[Discussions](https://github.com/Hawksight-AI/semantica/discussions)** - Questions and ideas
|
||||
|
||||
### Contact
|
||||
|
||||
- **GitHub Issues**: [Create an issue](https://github.com/Hawksight-AI/semantica/issues) for all communication
|
||||
- **GitHub Security Advisories**: [Report security issues](https://github.com/Hawksight-AI/semantica/security/advisories/new)
|
||||
### Security Issues
|
||||
- **[Report Security](https://github.com/Hawksight-AI/semantica/security/advisories/new)** - Security vulnerabilities
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Community Values
|
||||
## Community Guidelines
|
||||
|
||||
- **Respect**: Treat everyone with respect and kindness
|
||||
- **Inclusion**: Welcome people of all backgrounds
|
||||
- **Collaboration**: Work together to build something great
|
||||
- **Learning**: Share knowledge and help others
|
||||
- **Openness**: Transparent communication
|
||||
### Our Values
|
||||
- **Respect** - Treat everyone with kindness
|
||||
- **Inclusion** - Welcome all backgrounds and experience levels
|
||||
- **Collaboration** - Work together to build great things
|
||||
- **Learning** - Share knowledge and help others grow
|
||||
|
||||
---
|
||||
|
||||
## 📖 Code of Conduct
|
||||
|
||||
We have a [Code of Conduct](https://github.com/Hawksight-AI/semantica/blob/main/CODE_OF_CONDUCT.md) that all community members must follow.
|
||||
### Code of Conduct
|
||||
We follow the [Contributor Covenant Code of Conduct](https://github.com/Hawksight-AI/semantica/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
### Reporting Issues
|
||||
|
||||
If you experience unacceptable behavior:
|
||||
If you experience unacceptable behavior, please:
|
||||
1. Document what happened
|
||||
2. Contact maintainers through [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues) with "[CoC]" prefix
|
||||
2. Create an issue with "[CoC]" prefix
|
||||
3. We'll investigate and respond appropriately
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Getting Help
|
||||
## Contributing
|
||||
|
||||
### Before Asking
|
||||
### Ways to Contribute
|
||||
- **Code** - Fix bugs, add features, improve documentation
|
||||
- **Documentation** - Improve guides, fix typos, add examples
|
||||
- **Testing** - Report issues, write tests, validate fixes
|
||||
- **Community** - Help others, share knowledge, provide feedback
|
||||
|
||||
1. Check the [documentation](index.md)
|
||||
2. Search [GitHub issues](https://github.com/Hawksight-AI/semantica/issues)
|
||||
3. Review the [FAQ](faq.md)
|
||||
4. Check the [cookbook](cookbook.md)
|
||||
|
||||
### Asking Questions
|
||||
|
||||
When asking for help:
|
||||
- Be specific about your problem
|
||||
- Include environment details
|
||||
- Share what you've tried
|
||||
- Provide code examples
|
||||
- Be patient
|
||||
### Getting Started
|
||||
1. **Fork** the repository
|
||||
2. **Create** a feature branch
|
||||
3. **Make** your changes
|
||||
4. **Test** your changes
|
||||
5. **Submit** a pull request
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Recognition
|
||||
## Stay Connected
|
||||
|
||||
All contributors are recognized in:
|
||||
- [CONTRIBUTORS.md](https://github.com/Hawksight-AI/semantica/blob/main/CONTRIBUTORS.md)
|
||||
- GitHub contributors page
|
||||
- Release notes (for significant contributions)
|
||||
### Follow the Project
|
||||
- **[GitHub](https://github.com/Hawksight-AI/semantica)** - Source code and releases
|
||||
- **[PyPI](https://pypi.org/project/semantica/)** - Package information and downloads
|
||||
|
||||
### Share Your Work
|
||||
- **Blog Posts** - Write about your Semantica projects
|
||||
- **Tutorials** - Create guides and examples
|
||||
- **Projects** - Share what you've built with Semantica
|
||||
|
||||
---
|
||||
|
||||
## 📚 Resources
|
||||
## Need Help?
|
||||
|
||||
- **[Getting Started](getting-started.md)** - Quick start guide
|
||||
- **[FAQ](faq.md)** - Frequently asked questions
|
||||
- **[Contributing Guide](contributing.md)** - How to contribute
|
||||
- **[Governance](governance.md)** - Project governance
|
||||
- **[Community Projects](community-projects.md)** - Community showcase
|
||||
|
||||
---
|
||||
|
||||
!!! success "Thank You!"
|
||||
Thank you for being part of the Semantica community! 🎉
|
||||
- **[GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)** - Ask questions
|
||||
|
||||
+142
-2356
File diff suppressed because it is too large
Load Diff
+64
-91
@@ -1,126 +1,99 @@
|
||||
# Contributing to Semantica
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in contributing to Semantica!
|
||||
|
||||
!!! tip "Quick Start"
|
||||
New to contributing? Check out issues labeled [`good-first-issue`](https://github.com/Hawksight-AI/semantica/labels/good-first-issue)
|
||||
Contributions of all kinds are welcome — code, documentation, tests, and community support.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Essential Links
|
||||
## Quick Start
|
||||
|
||||
- **[Contributing Guide](https://github.com/Hawksight-AI/semantica/blob/main/CONTRIBUTING.md)** - Complete contribution guidelines
|
||||
- **[Code of Conduct](https://github.com/Hawksight-AI/semantica/blob/main/CODE_OF_CONDUCT.md)** - Community standards
|
||||
- **[Security Policy](https://github.com/Hawksight-AI/semantica/blob/main/SECURITY.md)** - Report vulnerabilities
|
||||
- **[GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)** - Bug reports and features
|
||||
```bash
|
||||
# Fork the repo, then:
|
||||
git clone https://github.com/your-username/semantica.git
|
||||
cd semantica
|
||||
pip install -e ".[dev]"
|
||||
pytest
|
||||
```
|
||||
|
||||
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
|
||||
## Ways to Contribute
|
||||
|
||||
### Code Contributions
|
||||
**Code**
|
||||
- Fix bugs and resolve open issues
|
||||
- Implement new features or integrations
|
||||
- Optimize performance or refactor existing code
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
**Documentation**
|
||||
- Fix typos, improve clarity, add examples
|
||||
- Write tutorials or domain-specific cookbook notebooks
|
||||
- Keep API reference up to date
|
||||
|
||||
See the [Contributing Guide](https://github.com/Hawksight-AI/semantica/blob/main/CONTRIBUTING.md) for detailed instructions.
|
||||
**Testing**
|
||||
- Add test coverage for untested modules
|
||||
- Reproduce and confirm reported bugs
|
||||
- Improve test reliability
|
||||
|
||||
### Documentation
|
||||
**Community**
|
||||
- Answer questions in issues and discussions
|
||||
- Review pull requests
|
||||
- Share Semantica in your blog posts or talks
|
||||
|
||||
- Fix typos and improve clarity
|
||||
- Add examples and tutorials
|
||||
- Update API documentation
|
||||
- Translate documentation
|
||||
---
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
### Bug Reports
|
||||
|
||||
Report bugs on [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues) with:
|
||||
- Description of the problem
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Environment details
|
||||
Include: what happened, steps to reproduce, expected behavior, and your environment (Python version, OS, Semantica version).
|
||||
|
||||
### Feature Requests
|
||||
|
||||
Suggest features on [GitHub Issues](https://github.com/Hawksight-AI/semantica/issues) with:
|
||||
- Use case description
|
||||
- Proposed solution
|
||||
- Benefits to the community
|
||||
Include: your use case, what you'd like Semantica to do, and how it benefits others.
|
||||
|
||||
---
|
||||
|
||||
## ✍️ Documentation Style Guide
|
||||
## Pull Request Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
Before submitting:
|
||||
|
||||
- Use clear, concise language
|
||||
- Include working code examples
|
||||
- Test all examples before submitting
|
||||
- Follow existing documentation structure
|
||||
- Use proper markdown formatting
|
||||
- [ ] 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
|
||||
|
||||
### API Documentation Format
|
||||
---
|
||||
|
||||
```python
|
||||
def function_name(
|
||||
param1: str,
|
||||
param2: int = 0
|
||||
) -> ReturnType:
|
||||
"""Brief description.
|
||||
|
||||
Args:
|
||||
param1: Description of param1
|
||||
param2: Description of param2 (default: 0)
|
||||
|
||||
Returns:
|
||||
Description of return value
|
||||
|
||||
Raises:
|
||||
ValueError: When and why this is raised
|
||||
|
||||
Example:
|
||||
>>> result = function_name("test", 5)
|
||||
>>> print(result)
|
||||
expected_output
|
||||
"""
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/semantica.git
|
||||
cd semantica
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
Code style tools used: **Black** (formatting), **isort** (imports), **flake8** (linting).
|
||||
|
||||
Run the full test suite:
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Documentation Structure
|
||||
## Community
|
||||
|
||||
```
|
||||
docs/
|
||||
├── index.md # Homepage
|
||||
├── getting-started.md # Getting started
|
||||
├── concepts.md # Core concepts
|
||||
├── modules.md # Module overview
|
||||
├── use-cases.md # Use cases
|
||||
├── examples.md # Examples
|
||||
├── cookbook/ # Tutorials
|
||||
└── reference/ # API reference
|
||||
```
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Documentation Tools
|
||||
## Help
|
||||
|
||||
- **[MkDocs](https://www.mkdocs.org/)** - Documentation generator
|
||||
- **[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)** - Theme
|
||||
- **[mkdocstrings](https://mkdocstrings.github.io/)** - API docs from docstrings
|
||||
- **[Mermaid](https://mermaid.js.org/)** - Diagrams
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Getting Help
|
||||
|
||||
- **[GitHub Issues](https://github.com/Hawksight-AI/semantica/issues)** - Ask questions
|
||||
- **Documentation** - Check existing docs for examples
|
||||
- **Pull Requests** - Review other contributors' PRs
|
||||
|
||||
---
|
||||
|
||||
!!! success "Thank You!"
|
||||
Every contribution helps make Semantica better! 🎉
|
||||
- [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:
|
||||
|
||||
|
||||
+87
-359
@@ -34,18 +34,18 @@ html {
|
||||
[data-md-color-scheme="slate"] {
|
||||
/* Dark Mode */
|
||||
--md-default-bg-color: #0F1115;
|
||||
/* Very dark grey, almost black */
|
||||
--md-default-fg-color: #E0E0E0;
|
||||
|
||||
--md-primary-fg-color: #0F1115;
|
||||
/* Match bg for seamless look or slightly lighter */
|
||||
--md-primary-fg-color--light: #212121;
|
||||
--md-primary-fg-color--dark: #000000;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
color: var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
/*
|
||||
==========================================================================
|
||||
Typography
|
||||
==========================================================================
|
||||
*/
|
||||
.md-typeset h2 {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
@@ -66,6 +66,12 @@ html {
|
||||
background-color: #F1F8F5;
|
||||
}
|
||||
|
||||
/*
|
||||
==========================================================================
|
||||
Admonitions
|
||||
==========================================================================
|
||||
*/
|
||||
/* Tip */
|
||||
.md-typeset .admonition.tip .admonition-title {
|
||||
color: #00C853;
|
||||
}
|
||||
@@ -137,7 +143,11 @@ html {
|
||||
border-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
/*
|
||||
==========================================================================
|
||||
Scrollbars
|
||||
==========================================================================
|
||||
*/
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -149,303 +159,7 @@ html {
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/*
|
||||
==========================================================================
|
||||
Version Selector
|
||||
==========================================================================
|
||||
*/
|
||||
.version-scroll-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1.5rem;
|
||||
/* Increased spacing */
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
padding: 4px 0;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
height: 100%;
|
||||
/* Match header height context */
|
||||
}
|
||||
|
||||
.version-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.version-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 12px;
|
||||
/* Larger touch target and better visibility */
|
||||
border-radius: 4px;
|
||||
/* Slightly more squared to match material design */
|
||||
font-size: 0.8rem;
|
||||
/* Slightly larger text */
|
||||
font-weight: 700;
|
||||
/* Bolder for visibility */
|
||||
line-height: 1.2;
|
||||
color: var(--md-default-fg-color);
|
||||
/* Darker text for contrast */
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
/* Slightly darker bg */
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
/* Subtle border */
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none !important;
|
||||
font-family: var(--md-text-font-family);
|
||||
}
|
||||
|
||||
.version-tag:hover {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
color: var(--md-primary-fg-color);
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.version-tag.active {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
color: white;
|
||||
border-color: var(--md-accent-fg-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
/* Subtle shadow for depth */
|
||||
}
|
||||
|
||||
/* Dark Mode Adjustments */
|
||||
[data-md-color-scheme="slate"] .version-tag {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: var(--md-default-fg-color);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .version-tag:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .version-tag.active {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
color: white;
|
||||
border-color: var(--md-accent-fg-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media screen and (max-width: 76.1875em) {
|
||||
.version-scroll-container {
|
||||
margin-left: 1rem;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
==========================================================================
|
||||
Footer Attribution - Keep MkDocs Credit Visible
|
||||
==========================================================================
|
||||
*/
|
||||
.md-footer-meta__inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.md-footer-copyright {
|
||||
opacity: 1 !important;
|
||||
color: var(--md-default-fg-color--light) !important;
|
||||
}
|
||||
|
||||
.md-footer-copyright__highlight {
|
||||
opacity: 1 !important;
|
||||
color: var(--md-default-fg-color) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
/* Warning */
|
||||
.md-typeset .admonition.warning {
|
||||
border-color: #E0E0E0;
|
||||
border-left-color: #FFAB00;
|
||||
background-color: #FFF8E1;
|
||||
}
|
||||
|
||||
.md-typeset .admonition.warning .admonition-title {
|
||||
color: #FFAB00;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset .admonition.warning {
|
||||
border-color: #2E303E;
|
||||
border-left-color: #FFD740;
|
||||
background-color: #1F1B0E;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset .admonition.warning .admonition-title {
|
||||
color: #FFD740;
|
||||
}
|
||||
|
||||
/* Danger */
|
||||
.md-typeset .admonition.danger {
|
||||
border-color: #E0E0E0;
|
||||
border-left-color: #FF1744;
|
||||
background-color: #FFEBEE;
|
||||
}
|
||||
|
||||
.md-typeset .admonition.danger .admonition-title {
|
||||
color: #FF1744;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset .admonition.danger {
|
||||
border-color: #2E303E;
|
||||
border-left-color: #FF5252;
|
||||
background-color: #241214;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset .admonition.danger .admonition-title {
|
||||
color: #FF5252;
|
||||
}
|
||||
|
||||
/*
|
||||
==========================================================================
|
||||
Code Blocks
|
||||
==========================================================================
|
||||
*/
|
||||
.md-typeset pre {
|
||||
background-color: var(--md-code-bg-color);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset pre {
|
||||
border-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/*
|
||||
==========================================================================
|
||||
Version Selector
|
||||
==========================================================================
|
||||
*/
|
||||
.version-scroll-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1.5rem;
|
||||
/* Increased spacing */
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
padding: 4px 0;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
height: 100%;
|
||||
/* Match header height context */
|
||||
}
|
||||
|
||||
.version-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.version-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 12px;
|
||||
/* Larger touch target and better visibility */
|
||||
border-radius: 4px;
|
||||
/* Slightly more squared to match material design */
|
||||
font-size: 0.8rem;
|
||||
/* Slightly larger text */
|
||||
font-weight: 700;
|
||||
/* Bolder for visibility */
|
||||
line-height: 1.2;
|
||||
color: var(--md-default-fg-color);
|
||||
/* Darker text for contrast */
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
/* Slightly darker bg */
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
/* Subtle border */
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none !important;
|
||||
font-family: var(--md-text-font-family);
|
||||
}
|
||||
|
||||
.version-tag:hover {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
color: var(--md-primary-fg-color);
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.version-tag.active {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
color: white;
|
||||
border-color: var(--md-accent-fg-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
/* Subtle shadow for depth */
|
||||
}
|
||||
|
||||
/* Dark Mode Adjustments */
|
||||
[data-md-color-scheme="slate"] .version-tag {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: var(--md-default-fg-color);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .version-tag:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .version-tag.active {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
color: white;
|
||||
border-color: var(--md-accent-fg-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media screen and (max-width: 76.1875em) {
|
||||
.version-scroll-container {
|
||||
margin-left: 1rem;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
background-color: #2962FF;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -484,7 +198,6 @@ html {
|
||||
Active Link Highlighting
|
||||
==========================================================================
|
||||
*/
|
||||
|
||||
/* Left Sidebar (Navigation) - Active Link */
|
||||
.md-nav__link--active {
|
||||
color: var(--md-accent-fg-color) !important;
|
||||
@@ -495,85 +208,100 @@ html {
|
||||
.md-nav__item--active > .md-nav__link {
|
||||
color: var(--md-accent-fg-color) !important;
|
||||
border-left: 2px solid var(--md-accent-fg-color);
|
||||
padding-left: 0.5rem; /* Adjust padding to look good with border */
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Ensure nested items in TOC don't inherit the border unless active themselves */
|
||||
.md-nav__item .md-nav__item--active > .md-nav__link {
|
||||
border-left: 2px solid var(--md-accent-fg-color);
|
||||
border-left: 2px solid var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
==========================================================================
|
||||
Layout Optimization
|
||||
==========================================================================
|
||||
Home Page Content Alignment - Left Align
|
||||
==========================================================================
|
||||
*/
|
||||
|
||||
/* 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 */
|
||||
@@ -584,4 +312,4 @@ html {
|
||||
/* Keep hero section centered */
|
||||
.md-typeset > div[align="center"] {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user