mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-01 04:00:28 +00:00
Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
244da368a9 | ||
|
|
46b18fbee3 | ||
|
|
b0679d4f67 | ||
|
|
d135ad185f | ||
|
|
96dbd3f0d4 | ||
|
|
d4cd44e7f1 | ||
|
|
832412cc01 | ||
|
|
dfbe98cebd | ||
|
|
89c6c8a45d | ||
|
|
4b24053851 | ||
|
|
c111277a1f | ||
|
|
5ed98cefbd | ||
|
|
48737629d9 | ||
|
|
1d62217cc5 | ||
|
|
bf292ccbbc | ||
|
|
64d942503b | ||
|
|
471a420b10 | ||
|
|
a4aa71ad87 | ||
|
|
fa87a1a9be | ||
|
|
08c78bfb40 | ||
|
|
56b174781f | ||
|
|
dfda4c561a | ||
|
|
ea0dd17bff | ||
|
|
f6cd62411b | ||
|
|
cac6dfbe45 | ||
|
|
80e9737542 | ||
|
|
14fb975fa1 | ||
|
|
8b0ac61afd | ||
|
|
9d98eedaa1 | ||
|
|
5ffb212a8f | ||
|
|
ca7f743dab | ||
|
|
5cf59fdd88 | ||
|
|
0384a8de30 | ||
|
|
f3d5932c24 | ||
|
|
a5aac7e22a | ||
|
|
3448ac0689 | ||
|
|
70dfbf151c | ||
|
|
47446ebdde | ||
|
|
c8a591e89e | ||
|
|
ab86127e4e | ||
|
|
30592b1285 | ||
|
|
aceb69a5bc | ||
|
|
e13c953bd8 | ||
|
|
85d6ccd0a5 | ||
|
|
dfd668c206 | ||
|
|
e74d0a274d | ||
|
|
b570794515 | ||
|
|
a94cec3b36 | ||
|
|
f9b1295d14 |
@@ -18,6 +18,9 @@
|
||||
.git/**
|
||||
.github
|
||||
.github/**
|
||||
!.github/requirements/
|
||||
!.github/requirements/explorer-extra-py313.txt
|
||||
!.github/requirements/pep517-build.txt
|
||||
.claude
|
||||
.claude/**
|
||||
.codex
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
name: 'Setup Semantica'
|
||||
description: 'Install Python, cache pip, and install the semantica package into a workflow'
|
||||
author: 'Semantica'
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: 'Python version to set up'
|
||||
required: false
|
||||
default: '3.11'
|
||||
version:
|
||||
description: 'Version constraint to append to the pip spec, e.g. "==0.6.7" or ">=0.6,<0.7". Leave empty for the latest release.'
|
||||
required: false
|
||||
default: ''
|
||||
extras:
|
||||
description: 'Comma-separated extras to install, e.g. "explorer,all"'
|
||||
required: false
|
||||
default: ''
|
||||
cache:
|
||||
description: 'Pip cache mode passed straight to actions/setup-python ("pip" to enable). Left empty (disabled) by default because this action is meant to run standalone in any caller repo, and actions/setup-python errors out if it cannot find a requirements.txt/pyproject.toml/setup.py/poetry.lock to key the cache on. Opt in only when the caller repo has one of those files.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
outputs:
|
||||
version:
|
||||
description: 'The installed semantica version'
|
||||
value: ${{ steps.verify.outputs.version }}
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
cache: ${{ inputs.cache }}
|
||||
|
||||
- name: Install semantica
|
||||
shell: bash
|
||||
env:
|
||||
SEMANTICA_EXTRAS: ${{ inputs.extras }}
|
||||
SEMANTICA_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -n "$SEMANTICA_EXTRAS" ]; then
|
||||
spec="semantica[$SEMANTICA_EXTRAS]$SEMANTICA_VERSION"
|
||||
else
|
||||
spec="semantica$SEMANTICA_VERSION"
|
||||
fi
|
||||
python -m pip install -- "$spec"
|
||||
|
||||
- name: Verify install
|
||||
id: verify
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(python -c "import semantica; print(semantica.__version__)")
|
||||
echo "Installed semantica $VERSION"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
@@ -101,6 +101,29 @@ updates:
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
|
||||
# Explorer frontend (npm)
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/explorer"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "03:30" # 3:30 AM UTC (9:00 AM IST)
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- "KaifAhmad1"
|
||||
assignees:
|
||||
- "KaifAhmad1"
|
||||
commit-message:
|
||||
prefix: "security"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
- "security"
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
- dependency-type: "development"
|
||||
|
||||
# Docker dependencies (if you use Docker)
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# CI tool requirements
|
||||
|
||||
Hash-pinned `pip install` targets for CI/release/Dockerfile steps that install
|
||||
something other than the project's own audited `requirements-ci.txt` set.
|
||||
These exist because OpenSSF Scorecard's Pinned-Dependencies check flags any
|
||||
`pip install` in a workflow or Dockerfile that isn't hash-verified, and
|
||||
`requirements-ci.txt` alone doesn't cover build/release/security tooling or
|
||||
the project's own local-source install.
|
||||
|
||||
Each `.txt` was generated from the adjacent `.in` (or, for `explorer-extra-py311.txt`,
|
||||
`explorer-extra-py313.txt`, and `base-deps.txt`, from `pyproject.toml` directly) with:
|
||||
|
||||
```
|
||||
uv pip compile <input> --python-version 3.11 --python-platform linux \
|
||||
--constraint requirements-ci.txt --generate-hashes -o <output>.txt
|
||||
```
|
||||
|
||||
(`--constraint requirements-ci.txt` is omitted for `bootstrap.txt`,
|
||||
`build-tools.txt`, `uv-tool.txt`, `twine.txt`, `pip-audit.txt`, and
|
||||
`security-scan-tools.txt`, since those install standalone tooling with no
|
||||
version relationship to the project's own dependency tree.)
|
||||
|
||||
Regenerate a file the same way after bumping a pinned version, and re-run it
|
||||
whenever `requirements-ci.txt` changes if the file used `--constraint` (see
|
||||
each file's own autogenerated header comment for its exact command).
|
||||
|
||||
| File | Used by | Installs |
|
||||
| --- | --- | --- |
|
||||
| `bootstrap.txt` | security.yml, security-scan.yml, benchmark.yml | pip, setuptools (upgrade before anything else) |
|
||||
| `pep517-build.txt` | ci.yml, benchmark.yml, Dockerfile | exact `[build-system] requires` from `pyproject.toml` (setuptools, wheel) - installed with `--no-build-isolation` before any `pip install -e .` / `pip install .`, since `--no-deps` alone doesn't stop pip's PEP 517 build isolation from fetching those two *unhashed* |
|
||||
| `explorer-extra-py311.txt` | ci.yml | semantica's base deps + the `explorer` extra, resolved for python 3.11 |
|
||||
| `explorer-extra-py313.txt` | Dockerfile | the same, resolved for python 3.13 (the image's actual interpreter) |
|
||||
| `pytest-tool.txt` | ci.yml | pytest, for the pre-all-extras deterministic test |
|
||||
| `uv-tool.txt` | ci.yml | uv, to verify requirements-ci.txt is current |
|
||||
| `build-tools.txt` | ci.yml, release.yml | build, wheel |
|
||||
| `twine.txt` | release.yml | twine |
|
||||
| `pip-audit.txt` | security.yml | pip-audit |
|
||||
| `security-scan-tools.txt` | security-scan.yml | safety, bandit, semgrep, jq |
|
||||
| `base-deps.txt` | benchmark.yml | semantica's base deps (no extras) |
|
||||
| `benchmark-extra.txt` | benchmark.yml | the benchmark-only libs (neo4j, pdfplumber, etc.) |
|
||||
|
||||
`explorer-extra-py31{1,3}.txt` and `base-deps.txt` are large (they mirror
|
||||
most of `requirements-ci.txt`) because semantica's `dependencies` list in
|
||||
`pyproject.toml` isn't extras-gated - installing the package at all pulls
|
||||
the full base set. That's expected, not a mistake.
|
||||
|
||||
`explorer-extra-py311.txt` and `explorer-extra-py313.txt` are **not**
|
||||
interchangeable, and can't be collapsed into one file compiled for either
|
||||
version: `librosa`'s `audioread` dependency needs `standard-aifc` /
|
||||
`standard-sunau` only under `python_version >= "3.13"` (Python 3.13 dropped
|
||||
`aifc`/`sunau` from stdlib). A file resolved for 3.11 simply omits those
|
||||
packages' hashes, so installing it with `--require-hashes` on a real 3.13
|
||||
interpreter (the Dockerfile's base image) fails outright rather than
|
||||
silently under-pinning. Any other file shared across a 3.11 and 3.13
|
||||
consumer would need the same split if it hits a similar stdlib-removal
|
||||
edge case - check for `ERROR: In --require-hashes mode, all requirements
|
||||
must have their versions pinned` on the *other* Python version before
|
||||
assuming one `--python-version` covers every consumer.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
rdflib
|
||||
neo4j
|
||||
faiss-cpu
|
||||
torch
|
||||
pyarrow
|
||||
pdfplumber
|
||||
python-pptx
|
||||
openpyxl
|
||||
lxml
|
||||
python-docx
|
||||
beautifulsoup4
|
||||
chardet
|
||||
langdetect
|
||||
en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
pip
|
||||
setuptools
|
||||
@@ -0,0 +1,10 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/bootstrap.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/bootstrap.txt
|
||||
pip==26.2.1 \
|
||||
--hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \
|
||||
--hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f
|
||||
# via -r .github/requirements/bootstrap.in
|
||||
setuptools==84.0.0 \
|
||||
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
|
||||
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
|
||||
# via -r .github/requirements/bootstrap.in
|
||||
@@ -0,0 +1,2 @@
|
||||
build==1.6.0
|
||||
wheel==0.48.0
|
||||
@@ -0,0 +1,20 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/build-tools.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/build-tools.txt
|
||||
build==1.6.0 \
|
||||
--hash=sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af \
|
||||
--hash=sha256:f7aaf1ebbb79178a02ba248bb524f2176b256017e17e8e4bd4289c7b38cc2bad
|
||||
# via -r .github/requirements/build-tools.in
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via
|
||||
# build
|
||||
# wheel
|
||||
pyproject-hooks==1.2.0 \
|
||||
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
|
||||
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
|
||||
# via build
|
||||
wheel==0.48.0 \
|
||||
--hash=sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab \
|
||||
--hash=sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322
|
||||
# via -r .github/requirements/build-tools.in
|
||||
@@ -0,0 +1 @@
|
||||
checkov==3.3.16
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
setuptools==84.0.0
|
||||
wheel==0.48.0
|
||||
@@ -0,0 +1,14 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/pep517-build.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/pep517-build.txt
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via wheel
|
||||
setuptools==84.0.0 \
|
||||
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
|
||||
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
|
||||
# via -r .github/requirements/pep517-build.in
|
||||
wheel==0.48.0 \
|
||||
--hash=sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab \
|
||||
--hash=sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322
|
||||
# via -r .github/requirements/pep517-build.in
|
||||
@@ -0,0 +1 @@
|
||||
pip-audit==2.10.1
|
||||
@@ -0,0 +1,423 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/pip-audit.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/pip-audit.txt
|
||||
boolean-py==5.0 \
|
||||
--hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \
|
||||
--hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9
|
||||
# via license-expression
|
||||
cachecontrol==0.14.4 \
|
||||
--hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \
|
||||
--hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1
|
||||
# via pip-audit
|
||||
certifi==2026.7.22 \
|
||||
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
|
||||
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
|
||||
# via requests
|
||||
charset-normalizer==3.5.1 \
|
||||
--hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \
|
||||
--hash=sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d \
|
||||
--hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \
|
||||
--hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \
|
||||
--hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \
|
||||
--hash=sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f \
|
||||
--hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \
|
||||
--hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \
|
||||
--hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \
|
||||
--hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \
|
||||
--hash=sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838 \
|
||||
--hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \
|
||||
--hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \
|
||||
--hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \
|
||||
--hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \
|
||||
--hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \
|
||||
--hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \
|
||||
--hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \
|
||||
--hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \
|
||||
--hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \
|
||||
--hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \
|
||||
--hash=sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8 \
|
||||
--hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \
|
||||
--hash=sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf \
|
||||
--hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \
|
||||
--hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \
|
||||
--hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \
|
||||
--hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \
|
||||
--hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \
|
||||
--hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \
|
||||
--hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \
|
||||
--hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \
|
||||
--hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \
|
||||
--hash=sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb \
|
||||
--hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \
|
||||
--hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \
|
||||
--hash=sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b \
|
||||
--hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \
|
||||
--hash=sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d \
|
||||
--hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \
|
||||
--hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \
|
||||
--hash=sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818 \
|
||||
--hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \
|
||||
--hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \
|
||||
--hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \
|
||||
--hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \
|
||||
--hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \
|
||||
--hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \
|
||||
--hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \
|
||||
--hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \
|
||||
--hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \
|
||||
--hash=sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a \
|
||||
--hash=sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420 \
|
||||
--hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \
|
||||
--hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \
|
||||
--hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \
|
||||
--hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \
|
||||
--hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \
|
||||
--hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \
|
||||
--hash=sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18 \
|
||||
--hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \
|
||||
--hash=sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f \
|
||||
--hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \
|
||||
--hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \
|
||||
--hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \
|
||||
--hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \
|
||||
--hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \
|
||||
--hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \
|
||||
--hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \
|
||||
--hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \
|
||||
--hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \
|
||||
--hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \
|
||||
--hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \
|
||||
--hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \
|
||||
--hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \
|
||||
--hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \
|
||||
--hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \
|
||||
--hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \
|
||||
--hash=sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f \
|
||||
--hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \
|
||||
--hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \
|
||||
--hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \
|
||||
--hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \
|
||||
--hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \
|
||||
--hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \
|
||||
--hash=sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f \
|
||||
--hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \
|
||||
--hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \
|
||||
--hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \
|
||||
--hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \
|
||||
--hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \
|
||||
--hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \
|
||||
--hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \
|
||||
--hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \
|
||||
--hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \
|
||||
--hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \
|
||||
--hash=sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20 \
|
||||
--hash=sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f \
|
||||
--hash=sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3 \
|
||||
--hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \
|
||||
--hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \
|
||||
--hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \
|
||||
--hash=sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2 \
|
||||
--hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \
|
||||
--hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \
|
||||
--hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \
|
||||
--hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \
|
||||
--hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \
|
||||
--hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \
|
||||
--hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \
|
||||
--hash=sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa \
|
||||
--hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \
|
||||
--hash=sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5 \
|
||||
--hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \
|
||||
--hash=sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d \
|
||||
--hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \
|
||||
--hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \
|
||||
--hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \
|
||||
--hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \
|
||||
--hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \
|
||||
--hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \
|
||||
--hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \
|
||||
--hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \
|
||||
--hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \
|
||||
--hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \
|
||||
--hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \
|
||||
--hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \
|
||||
--hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \
|
||||
--hash=sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab \
|
||||
--hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \
|
||||
--hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \
|
||||
--hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \
|
||||
--hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \
|
||||
--hash=sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d \
|
||||
--hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \
|
||||
--hash=sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea \
|
||||
--hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \
|
||||
--hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \
|
||||
--hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \
|
||||
--hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \
|
||||
--hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \
|
||||
--hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \
|
||||
--hash=sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a \
|
||||
--hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \
|
||||
--hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \
|
||||
--hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \
|
||||
--hash=sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa \
|
||||
--hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \
|
||||
--hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \
|
||||
--hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \
|
||||
--hash=sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795 \
|
||||
--hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \
|
||||
--hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \
|
||||
--hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \
|
||||
--hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \
|
||||
--hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \
|
||||
--hash=sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda \
|
||||
--hash=sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17 \
|
||||
--hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \
|
||||
--hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \
|
||||
--hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \
|
||||
--hash=sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182 \
|
||||
--hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \
|
||||
--hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \
|
||||
--hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \
|
||||
--hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \
|
||||
--hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \
|
||||
--hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \
|
||||
--hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \
|
||||
--hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \
|
||||
--hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \
|
||||
--hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f
|
||||
# via requests
|
||||
cyclonedx-python-lib==11.12.0 \
|
||||
--hash=sha256:0e807521a921a5c3cb8ce1153f8a61d29eedfe76a46aac2796b7c6b573391a54 \
|
||||
--hash=sha256:16767c4039de90c04e9f03348f8f0ed4b8ff842eaa7eefcad3a95685f970dacf
|
||||
# via pip-audit
|
||||
defusedxml==0.7.1 \
|
||||
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
|
||||
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
|
||||
# via py-serializable
|
||||
filelock==3.32.4 \
|
||||
--hash=sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd \
|
||||
--hash=sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30
|
||||
# via cachecontrol
|
||||
idna==3.19 \
|
||||
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
|
||||
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
|
||||
# via requests
|
||||
license-expression==30.4.4 \
|
||||
--hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \
|
||||
--hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd
|
||||
# via cyclonedx-python-lib
|
||||
markdown-it-py==4.2.0 \
|
||||
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
|
||||
# via rich
|
||||
mdurl==0.1.2 \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
msgpack==1.2.2 \
|
||||
--hash=sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516 \
|
||||
--hash=sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9 \
|
||||
--hash=sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c \
|
||||
--hash=sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3 \
|
||||
--hash=sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77 \
|
||||
--hash=sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4 \
|
||||
--hash=sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f \
|
||||
--hash=sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986 \
|
||||
--hash=sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f \
|
||||
--hash=sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc \
|
||||
--hash=sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc \
|
||||
--hash=sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34 \
|
||||
--hash=sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563 \
|
||||
--hash=sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c \
|
||||
--hash=sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a \
|
||||
--hash=sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808 \
|
||||
--hash=sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb \
|
||||
--hash=sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b \
|
||||
--hash=sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782 \
|
||||
--hash=sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367 \
|
||||
--hash=sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e \
|
||||
--hash=sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761 \
|
||||
--hash=sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648 \
|
||||
--hash=sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839 \
|
||||
--hash=sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35 \
|
||||
--hash=sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc \
|
||||
--hash=sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3 \
|
||||
--hash=sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab \
|
||||
--hash=sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23 \
|
||||
--hash=sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84 \
|
||||
--hash=sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7 \
|
||||
--hash=sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3 \
|
||||
--hash=sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640 \
|
||||
--hash=sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5 \
|
||||
--hash=sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839 \
|
||||
--hash=sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b \
|
||||
--hash=sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695 \
|
||||
--hash=sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d \
|
||||
--hash=sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f \
|
||||
--hash=sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923 \
|
||||
--hash=sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8 \
|
||||
--hash=sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970 \
|
||||
--hash=sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3 \
|
||||
--hash=sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683 \
|
||||
--hash=sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec \
|
||||
--hash=sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77 \
|
||||
--hash=sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949 \
|
||||
--hash=sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533 \
|
||||
--hash=sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc \
|
||||
--hash=sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe \
|
||||
--hash=sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba \
|
||||
--hash=sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff \
|
||||
--hash=sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36 \
|
||||
--hash=sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5 \
|
||||
--hash=sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c \
|
||||
--hash=sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929 \
|
||||
--hash=sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e \
|
||||
--hash=sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b \
|
||||
--hash=sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a \
|
||||
--hash=sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5 \
|
||||
--hash=sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754 \
|
||||
--hash=sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd \
|
||||
--hash=sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90 \
|
||||
--hash=sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea \
|
||||
--hash=sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f \
|
||||
--hash=sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212 \
|
||||
--hash=sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3 \
|
||||
--hash=sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21 \
|
||||
--hash=sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3 \
|
||||
--hash=sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54 \
|
||||
--hash=sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7 \
|
||||
--hash=sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc \
|
||||
--hash=sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b \
|
||||
--hash=sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f \
|
||||
--hash=sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b \
|
||||
--hash=sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896 \
|
||||
--hash=sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248 \
|
||||
--hash=sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9 \
|
||||
--hash=sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b \
|
||||
--hash=sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8 \
|
||||
--hash=sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9 \
|
||||
--hash=sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5 \
|
||||
--hash=sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf \
|
||||
--hash=sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04 \
|
||||
--hash=sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba \
|
||||
--hash=sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650 \
|
||||
--hash=sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f \
|
||||
--hash=sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22
|
||||
# via cachecontrol
|
||||
packageurl-python==0.17.6 \
|
||||
--hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \
|
||||
--hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9
|
||||
# via cyclonedx-python-lib
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via
|
||||
# pip-audit
|
||||
# pip-requirements-parser
|
||||
pip==26.2.1 \
|
||||
--hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \
|
||||
--hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f
|
||||
# via pip-api
|
||||
pip-api==0.0.34 \
|
||||
--hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \
|
||||
--hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625
|
||||
# via pip-audit
|
||||
pip-audit==2.10.1 \
|
||||
--hash=sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc \
|
||||
--hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a
|
||||
# via -r .github/requirements/pip-audit.in
|
||||
pip-requirements-parser==32.0.1 \
|
||||
--hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \
|
||||
--hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3
|
||||
# via pip-audit
|
||||
platformdirs==4.11.5 \
|
||||
--hash=sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b \
|
||||
--hash=sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173
|
||||
# via pip-audit
|
||||
py-serializable==2.1.0 \
|
||||
--hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \
|
||||
--hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304
|
||||
# via cyclonedx-python-lib
|
||||
pygments==2.21.0 \
|
||||
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
|
||||
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
|
||||
# via rich
|
||||
pyparsing==3.3.2 \
|
||||
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
|
||||
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
|
||||
# via pip-requirements-parser
|
||||
requests==2.34.2 \
|
||||
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||
# via
|
||||
# cachecontrol
|
||||
# pip-audit
|
||||
rich==15.0.0 \
|
||||
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
|
||||
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
|
||||
# via pip-audit
|
||||
sortedcontainers==2.4.0 \
|
||||
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
|
||||
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
|
||||
# via cyclonedx-python-lib
|
||||
tomli==2.4.1 \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
--hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
|
||||
--hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
|
||||
--hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
|
||||
--hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
|
||||
--hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
|
||||
--hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
|
||||
--hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
|
||||
--hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
|
||||
--hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
|
||||
--hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
|
||||
--hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
|
||||
--hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
|
||||
--hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
|
||||
--hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
|
||||
--hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
|
||||
--hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
|
||||
--hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
|
||||
--hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
|
||||
--hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
|
||||
--hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
|
||||
--hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
|
||||
--hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
|
||||
--hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
|
||||
--hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
|
||||
--hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
|
||||
--hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
|
||||
--hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
|
||||
--hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
|
||||
--hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
|
||||
--hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
|
||||
--hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
|
||||
--hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
|
||||
--hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
|
||||
--hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
|
||||
--hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
|
||||
--hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
|
||||
--hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
|
||||
--hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
|
||||
--hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
|
||||
--hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
|
||||
--hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
|
||||
--hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
|
||||
--hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
|
||||
--hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
|
||||
--hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
|
||||
# via pip-audit
|
||||
tomli-w==1.2.0 \
|
||||
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||
# via pip-audit
|
||||
typing-extensions==4.16.0 \
|
||||
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
|
||||
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
|
||||
# via cyclonedx-python-lib
|
||||
urllib3==2.7.0 \
|
||||
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via requests
|
||||
@@ -0,0 +1 @@
|
||||
pytest==9.1.1
|
||||
@@ -0,0 +1,32 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/pytest-tool.in --generate-hashes --python-version 3.11 --python-platform linux --constraint requirements-ci.txt -o .github/requirements/pytest-tool.txt
|
||||
iniconfig==2.3.0 \
|
||||
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
|
||||
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
pluggy==1.6.0 \
|
||||
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
pytest==9.1.1 \
|
||||
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
|
||||
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# -r .github/requirements/pytest-tool.in
|
||||
@@ -0,0 +1,4 @@
|
||||
safety==3.8.1
|
||||
bandit==1.9.4
|
||||
semgrep==1.175.0
|
||||
jq==1.12.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
twine==7.0.0
|
||||
@@ -0,0 +1,470 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/twine.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/twine.txt
|
||||
backports-tarfile==1.2.0 \
|
||||
--hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \
|
||||
--hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991
|
||||
# via jaraco-context
|
||||
certifi==2026.7.22 \
|
||||
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
|
||||
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
|
||||
# via requests
|
||||
cffi==2.1.1 \
|
||||
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
|
||||
--hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \
|
||||
--hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \
|
||||
--hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \
|
||||
--hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \
|
||||
--hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \
|
||||
--hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \
|
||||
--hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \
|
||||
--hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \
|
||||
--hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \
|
||||
--hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \
|
||||
--hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \
|
||||
--hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \
|
||||
--hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \
|
||||
--hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \
|
||||
--hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \
|
||||
--hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \
|
||||
--hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \
|
||||
--hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \
|
||||
--hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \
|
||||
--hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \
|
||||
--hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \
|
||||
--hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \
|
||||
--hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \
|
||||
--hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \
|
||||
--hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \
|
||||
--hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \
|
||||
--hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \
|
||||
--hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \
|
||||
--hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \
|
||||
--hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \
|
||||
--hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \
|
||||
--hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \
|
||||
--hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \
|
||||
--hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \
|
||||
--hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \
|
||||
--hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \
|
||||
--hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \
|
||||
--hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \
|
||||
--hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \
|
||||
--hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \
|
||||
--hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \
|
||||
--hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \
|
||||
--hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \
|
||||
--hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \
|
||||
--hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \
|
||||
--hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \
|
||||
--hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \
|
||||
--hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \
|
||||
--hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \
|
||||
--hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \
|
||||
--hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \
|
||||
--hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \
|
||||
--hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \
|
||||
--hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \
|
||||
--hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \
|
||||
--hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \
|
||||
--hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \
|
||||
--hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \
|
||||
--hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \
|
||||
--hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \
|
||||
--hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \
|
||||
--hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \
|
||||
--hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \
|
||||
--hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \
|
||||
--hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \
|
||||
--hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \
|
||||
--hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \
|
||||
--hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \
|
||||
--hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \
|
||||
--hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \
|
||||
--hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \
|
||||
--hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \
|
||||
--hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \
|
||||
--hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \
|
||||
--hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \
|
||||
--hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \
|
||||
--hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \
|
||||
--hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \
|
||||
--hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \
|
||||
--hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \
|
||||
--hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \
|
||||
--hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \
|
||||
--hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \
|
||||
--hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \
|
||||
--hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \
|
||||
--hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \
|
||||
--hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \
|
||||
--hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \
|
||||
--hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \
|
||||
--hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \
|
||||
--hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \
|
||||
--hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \
|
||||
--hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \
|
||||
--hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \
|
||||
--hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \
|
||||
--hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \
|
||||
--hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \
|
||||
--hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \
|
||||
--hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264
|
||||
# via cryptography
|
||||
charset-normalizer==3.5.1 \
|
||||
--hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \
|
||||
--hash=sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d \
|
||||
--hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \
|
||||
--hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \
|
||||
--hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \
|
||||
--hash=sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f \
|
||||
--hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \
|
||||
--hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \
|
||||
--hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \
|
||||
--hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \
|
||||
--hash=sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838 \
|
||||
--hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \
|
||||
--hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \
|
||||
--hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \
|
||||
--hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \
|
||||
--hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \
|
||||
--hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \
|
||||
--hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \
|
||||
--hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \
|
||||
--hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \
|
||||
--hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \
|
||||
--hash=sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8 \
|
||||
--hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \
|
||||
--hash=sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf \
|
||||
--hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \
|
||||
--hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \
|
||||
--hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \
|
||||
--hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \
|
||||
--hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \
|
||||
--hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \
|
||||
--hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \
|
||||
--hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \
|
||||
--hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \
|
||||
--hash=sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb \
|
||||
--hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \
|
||||
--hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \
|
||||
--hash=sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b \
|
||||
--hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \
|
||||
--hash=sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d \
|
||||
--hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \
|
||||
--hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \
|
||||
--hash=sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818 \
|
||||
--hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \
|
||||
--hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \
|
||||
--hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \
|
||||
--hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \
|
||||
--hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \
|
||||
--hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \
|
||||
--hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \
|
||||
--hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \
|
||||
--hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \
|
||||
--hash=sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a \
|
||||
--hash=sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420 \
|
||||
--hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \
|
||||
--hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \
|
||||
--hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \
|
||||
--hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \
|
||||
--hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \
|
||||
--hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \
|
||||
--hash=sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18 \
|
||||
--hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \
|
||||
--hash=sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f \
|
||||
--hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \
|
||||
--hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \
|
||||
--hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \
|
||||
--hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \
|
||||
--hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \
|
||||
--hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \
|
||||
--hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \
|
||||
--hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \
|
||||
--hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \
|
||||
--hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \
|
||||
--hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \
|
||||
--hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \
|
||||
--hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \
|
||||
--hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \
|
||||
--hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \
|
||||
--hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \
|
||||
--hash=sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f \
|
||||
--hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \
|
||||
--hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \
|
||||
--hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \
|
||||
--hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \
|
||||
--hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \
|
||||
--hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \
|
||||
--hash=sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f \
|
||||
--hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \
|
||||
--hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \
|
||||
--hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \
|
||||
--hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \
|
||||
--hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \
|
||||
--hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \
|
||||
--hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \
|
||||
--hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \
|
||||
--hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \
|
||||
--hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \
|
||||
--hash=sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20 \
|
||||
--hash=sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f \
|
||||
--hash=sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3 \
|
||||
--hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \
|
||||
--hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \
|
||||
--hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \
|
||||
--hash=sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2 \
|
||||
--hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \
|
||||
--hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \
|
||||
--hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \
|
||||
--hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \
|
||||
--hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \
|
||||
--hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \
|
||||
--hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \
|
||||
--hash=sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa \
|
||||
--hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \
|
||||
--hash=sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5 \
|
||||
--hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \
|
||||
--hash=sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d \
|
||||
--hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \
|
||||
--hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \
|
||||
--hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \
|
||||
--hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \
|
||||
--hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \
|
||||
--hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \
|
||||
--hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \
|
||||
--hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \
|
||||
--hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \
|
||||
--hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \
|
||||
--hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \
|
||||
--hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \
|
||||
--hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \
|
||||
--hash=sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab \
|
||||
--hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \
|
||||
--hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \
|
||||
--hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \
|
||||
--hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \
|
||||
--hash=sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d \
|
||||
--hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \
|
||||
--hash=sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea \
|
||||
--hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \
|
||||
--hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \
|
||||
--hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \
|
||||
--hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \
|
||||
--hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \
|
||||
--hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \
|
||||
--hash=sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a \
|
||||
--hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \
|
||||
--hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \
|
||||
--hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \
|
||||
--hash=sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa \
|
||||
--hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \
|
||||
--hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \
|
||||
--hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \
|
||||
--hash=sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795 \
|
||||
--hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \
|
||||
--hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \
|
||||
--hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \
|
||||
--hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \
|
||||
--hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \
|
||||
--hash=sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda \
|
||||
--hash=sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17 \
|
||||
--hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \
|
||||
--hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \
|
||||
--hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \
|
||||
--hash=sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182 \
|
||||
--hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \
|
||||
--hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \
|
||||
--hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \
|
||||
--hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \
|
||||
--hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \
|
||||
--hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \
|
||||
--hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \
|
||||
--hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \
|
||||
--hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \
|
||||
--hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f
|
||||
# via requests
|
||||
cryptography==50.0.1 \
|
||||
--hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \
|
||||
--hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \
|
||||
--hash=sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6 \
|
||||
--hash=sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e \
|
||||
--hash=sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361 \
|
||||
--hash=sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054 \
|
||||
--hash=sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f \
|
||||
--hash=sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6 \
|
||||
--hash=sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49 \
|
||||
--hash=sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5 \
|
||||
--hash=sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149 \
|
||||
--hash=sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88 \
|
||||
--hash=sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad \
|
||||
--hash=sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a \
|
||||
--hash=sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f \
|
||||
--hash=sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2 \
|
||||
--hash=sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20 \
|
||||
--hash=sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45 \
|
||||
--hash=sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f \
|
||||
--hash=sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b \
|
||||
--hash=sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527 \
|
||||
--hash=sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3 \
|
||||
--hash=sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6 \
|
||||
--hash=sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367 \
|
||||
--hash=sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0 \
|
||||
--hash=sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94 \
|
||||
--hash=sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239 \
|
||||
--hash=sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b \
|
||||
--hash=sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a \
|
||||
--hash=sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9 \
|
||||
--hash=sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5 \
|
||||
--hash=sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc \
|
||||
--hash=sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648 \
|
||||
--hash=sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986 \
|
||||
--hash=sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959 \
|
||||
--hash=sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0 \
|
||||
--hash=sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17 \
|
||||
--hash=sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e \
|
||||
--hash=sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733 \
|
||||
--hash=sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f \
|
||||
--hash=sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8 \
|
||||
--hash=sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf \
|
||||
--hash=sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671 \
|
||||
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
|
||||
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
|
||||
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
|
||||
# via secretstorage
|
||||
docutils==0.23 \
|
||||
--hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \
|
||||
--hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e
|
||||
# via readme-renderer
|
||||
id==1.6.1 \
|
||||
--hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \
|
||||
--hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca
|
||||
# via twine
|
||||
idna==3.19 \
|
||||
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
|
||||
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
|
||||
# via requests
|
||||
importlib-metadata==9.0.1 \
|
||||
--hash=sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99 \
|
||||
--hash=sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0
|
||||
# via keyring
|
||||
jaraco-classes==3.4.0 \
|
||||
--hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \
|
||||
--hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790
|
||||
# via keyring
|
||||
jaraco-context==6.1.2 \
|
||||
--hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \
|
||||
--hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3
|
||||
# via keyring
|
||||
jaraco-functools==4.6.0 \
|
||||
--hash=sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280 \
|
||||
--hash=sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30
|
||||
# via keyring
|
||||
jeepney==0.9.0 \
|
||||
--hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \
|
||||
--hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732
|
||||
# via
|
||||
# keyring
|
||||
# secretstorage
|
||||
keyring==25.7.0 \
|
||||
--hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \
|
||||
--hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b
|
||||
# via twine
|
||||
markdown-it-py==4.2.0 \
|
||||
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
|
||||
# via rich
|
||||
mdurl==0.1.2 \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
more-itertools==11.1.0 \
|
||||
--hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \
|
||||
--hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192
|
||||
# via
|
||||
# jaraco-classes
|
||||
# jaraco-functools
|
||||
nh3==0.3.7 \
|
||||
--hash=sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877 \
|
||||
--hash=sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62 \
|
||||
--hash=sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102 \
|
||||
--hash=sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086 \
|
||||
--hash=sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32 \
|
||||
--hash=sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8 \
|
||||
--hash=sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d \
|
||||
--hash=sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc \
|
||||
--hash=sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af \
|
||||
--hash=sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5 \
|
||||
--hash=sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59 \
|
||||
--hash=sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42 \
|
||||
--hash=sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848 \
|
||||
--hash=sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a \
|
||||
--hash=sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946 \
|
||||
--hash=sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba \
|
||||
--hash=sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac \
|
||||
--hash=sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174 \
|
||||
--hash=sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493 \
|
||||
--hash=sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa \
|
||||
--hash=sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd \
|
||||
--hash=sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563 \
|
||||
--hash=sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506 \
|
||||
--hash=sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b \
|
||||
--hash=sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a \
|
||||
--hash=sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479 \
|
||||
--hash=sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac
|
||||
# via readme-renderer
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via twine
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
# via cffi
|
||||
pygments==2.21.0 \
|
||||
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
|
||||
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
|
||||
# via
|
||||
# readme-renderer
|
||||
# rich
|
||||
readme-renderer==46.0 \
|
||||
--hash=sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90 \
|
||||
--hash=sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560
|
||||
# via twine
|
||||
requests==2.34.2 \
|
||||
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||
# via
|
||||
# requests-toolbelt
|
||||
# twine
|
||||
requests-toolbelt==1.0.0 \
|
||||
--hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \
|
||||
--hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06
|
||||
# via twine
|
||||
rfc3986==2.0.0 \
|
||||
--hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \
|
||||
--hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c
|
||||
# via twine
|
||||
rich==15.0.0 \
|
||||
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
|
||||
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
|
||||
# via twine
|
||||
secretstorage==3.5.0 \
|
||||
--hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \
|
||||
--hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be
|
||||
# via keyring
|
||||
twine==7.0.0 \
|
||||
--hash=sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177 \
|
||||
--hash=sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7
|
||||
# via -r .github/requirements/twine.in
|
||||
urllib3==2.7.0 \
|
||||
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via
|
||||
# id
|
||||
# requests
|
||||
# twine
|
||||
zipp==4.1.0 \
|
||||
--hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \
|
||||
--hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602
|
||||
# via importlib-metadata
|
||||
@@ -0,0 +1 @@
|
||||
uv==0.12.1
|
||||
@@ -0,0 +1,23 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/uv-tool.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/uv-tool.txt
|
||||
uv==0.12.1 \
|
||||
--hash=sha256:04290ea4001dca31ac8a8324113a4930dccad69ce35dbf6eaae307d54880890d \
|
||||
--hash=sha256:153ec0959a15397514438aefc1d7cd04235f335dd6bb53ea0f9e6e82c5a49f03 \
|
||||
--hash=sha256:173ee216f17d89fc39f65339d311a53584fc7de4918d27c0f3c7edafabc6b54d \
|
||||
--hash=sha256:1de49d9b04438f1ad2f41a1441dbbe19e230b94fca56d632818cfaed69e03bfc \
|
||||
--hash=sha256:1e8fd95fe98768e29436ad57f9ef7b68dc294b7b9862ef63396af8b15ab85e6c \
|
||||
--hash=sha256:27211df9b277f440dea438a4e525ba40250fb721ad39b8927eefc2d91f9aea15 \
|
||||
--hash=sha256:29399e1e73b67ed24abe82bc971aa4eb8419c4de804784290f39cf681f0b51ce \
|
||||
--hash=sha256:2e9b0b86e180abc5968b979c6e25203b32e85969abb5083ee1e8b88a5aa98a76 \
|
||||
--hash=sha256:3bd5db002adc763aa8d277f5b44f8d6e3fd82d20f2e51225b0bbdae1badc7259 \
|
||||
--hash=sha256:41b8fc2335f682312a1ca39a7b4abfd6af800992065c663582ca3e4d51cf9258 \
|
||||
--hash=sha256:5bd04849dd5346517cc4e57b4b3aa0b01c67c423878260c04f5893a038fe25b6 \
|
||||
--hash=sha256:6f7e72543264d2420ebb2ddc84696a751af2d6c5910046b7666589118f47292b \
|
||||
--hash=sha256:71f86410264c69a3e8acd18171897dd8ab1a13350cf40f718e4def5db2b724be \
|
||||
--hash=sha256:76d87de420213ca92fa403e87023c4c7c6956c6726c6b96d91c42cfe620173a3 \
|
||||
--hash=sha256:9331dda0dc4990512c232f86e1d3a7b83c13f459777fcc2bd46030911b40eaaa \
|
||||
--hash=sha256:b255ac23958e45f39f9c7a4cd65890df5ef46f539a3b14de03bd296bbba9cb60 \
|
||||
--hash=sha256:bd02f2da212e6a983115dc64a6fc94e9256c2d60e056d6b669de0a6025aaec05 \
|
||||
--hash=sha256:e35e0030480a8c3bf8ecd87ae4a6f6a224009e15e96a6fbb3634ac11ab75d582 \
|
||||
--hash=sha256:ead7ad064f291a5df358c3ffa8ffab347a32bd5a75a6a068ca22254c2539a829
|
||||
# via -r .github/requirements/uv-tool.in
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Drop checkov-suppressed results from its SARIF output before upload.
|
||||
|
||||
checkov's SARIF exporter includes every evaluated check as an ordinary
|
||||
result, including ones it internally marked SKIPPED via an inline
|
||||
`# checkov:skip=` comment or a `checkov.io/skipN` resource annotation - it
|
||||
never uses SARIF's `suppressions` field, and never drops them. checkov's
|
||||
JSON output *does* correctly record which checks were skipped, so this
|
||||
cross-references the two: any SARIF result whose (check_id, file) pair
|
||||
appears in the JSON's skipped_checks is removed before GitHub ever sees it.
|
||||
|
||||
Without this, every already-suppressed finding reopens as a brand new code
|
||||
scanning alert on every run, forever (see #6035/#6036, #6112-6115,
|
||||
#6128-6131 for the pattern this was chasing before this script existed).
|
||||
|
||||
Usage: filter_checkov_skipped.py <json_path> <sarif_in_path> <sarif_out_path>
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def path_suffix(path: str, segments: int = 2) -> str:
|
||||
"""Last N path segments, normalized to forward slashes, lowercased.
|
||||
|
||||
checkov's JSON file_path and SARIF artifactLocation.uri are relative to
|
||||
different roots (the scanned directory vs. a temp helm-render dir), so
|
||||
they can't be compared directly - but the last couple of segments
|
||||
(e.g. "templates/service.yaml") are stable across both and specific
|
||||
enough in practice to avoid cross-file collisions.
|
||||
"""
|
||||
normalized = path.replace("\\", "/").strip("/")
|
||||
return "/".join(normalized.split("/")[-segments:]).lower()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
json_path, sarif_in_path, sarif_out_path = sys.argv[1:4]
|
||||
|
||||
with open(json_path, encoding="utf-8") as f:
|
||||
checkov_json = json.load(f)
|
||||
if isinstance(checkov_json, dict):
|
||||
checkov_json = [checkov_json]
|
||||
|
||||
skipped = set()
|
||||
for block in checkov_json:
|
||||
for check in block.get("results", {}).get("skipped_checks", []):
|
||||
skipped.add((check["check_id"], path_suffix(check["file_path"])))
|
||||
|
||||
with open(sarif_in_path, encoding="utf-8") as f:
|
||||
sarif = json.load(f)
|
||||
|
||||
removed = 0
|
||||
for run in sarif.get("runs", []):
|
||||
kept = []
|
||||
for result in run.get("results", []):
|
||||
rule_id = result.get("ruleId")
|
||||
locations = result.get("locations") or [{}]
|
||||
uri = (
|
||||
locations[0]
|
||||
.get("physicalLocation", {})
|
||||
.get("artifactLocation", {})
|
||||
.get("uri", "")
|
||||
)
|
||||
if (rule_id, path_suffix(uri)) in skipped:
|
||||
removed += 1
|
||||
continue
|
||||
kept.append(result)
|
||||
run["results"] = kept
|
||||
|
||||
with open(sarif_out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(sarif, f)
|
||||
|
||||
print(f"Removed {removed} checkov-suppressed result(s) from the SARIF before upload.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -28,11 +28,37 @@ jobs:
|
||||
|
||||
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
|
||||
pip install -r .github/requirements/bootstrap.txt --require-hashes
|
||||
# --no-deps + a hash-pinned install of the same base dependency set
|
||||
# (rather than a bare `pip install -e .`) so every fetched package
|
||||
# is hash-verified (Scorecard Pinned-Dependencies); the local
|
||||
# editable install itself has nothing to hash.
|
||||
#
|
||||
# --no-deps only skips *runtime* dependency resolution - `-e .`
|
||||
# still does a PEP 517 build, which by default creates an isolated
|
||||
# build env and fetches [build-system] requires (setuptools,
|
||||
# wheel) completely outside any hash checking. Install
|
||||
# pep517-build.txt (pins that exact build-system.requires) first
|
||||
# and pass --no-build-isolation so pip reuses those hash-verified
|
||||
# copies instead of fetching its own.
|
||||
pip install -r .github/requirements/pep517-build.txt --require-hashes
|
||||
pip install --no-deps --no-build-isolation -e .
|
||||
pip install -r .github/requirements/base-deps.txt --require-hashes
|
||||
# NOTE: benchmarks/ does not currently exist in this repo (neither
|
||||
# requirements.txt nor benchmarks_runner.py below), so this job
|
||||
# already fails on any real invocation - pre-existing, unrelated to
|
||||
# this pinning change. The `pip install -r benchmarks/requirements.txt`
|
||||
# step that used to be here is dropped rather than fixed: there's
|
||||
# nothing to hash-pin without knowing what that file should
|
||||
# contain, and an unpinned install here would just re-trip
|
||||
# Scorecard's Pinned-Dependencies check for no real benefit, since
|
||||
# the job can't run to completion regardless.
|
||||
#
|
||||
# `python -m spacy download en_core_web_sm` fetches an unpinned,
|
||||
# unhashed wheel from spacy-models' GitHub releases - replaced with
|
||||
# a hash-pinned direct-URL install of the same 3.8.0 model (matches
|
||||
# the spacy==3.8.15 pinned in base-deps.txt) via benchmark-extra.txt.
|
||||
pip install -r .github/requirements/benchmark-extra.txt --require-hashes
|
||||
|
||||
- name: Execute Benchmarks (Real Mode)
|
||||
env:
|
||||
|
||||
@@ -52,16 +52,39 @@ jobs:
|
||||
# environment is installed. The Explorer extra supplies the
|
||||
# production API dependencies without importing optional vector
|
||||
# providers such as Pinecone during test collection.
|
||||
pip install -e ".[explorer]" pytest==9.1.1
|
||||
#
|
||||
# --no-deps + a separate hash-pinned install (rather than the old
|
||||
# `pip install -e ".[explorer]" pytest==9.1.1`) so every fetched
|
||||
# package is hash-verified (Scorecard Pinned-Dependencies); the
|
||||
# local editable install itself has nothing to hash.
|
||||
# .github/requirements/explorer-extra-py311.txt is
|
||||
# `uv pip compile pyproject.toml --extra explorer --python-version 3.11 --constraint requirements-ci.txt --generate-hashes`
|
||||
# - regenerate it the same way if pyproject.toml's base/explorer
|
||||
# deps change. Resolved specifically for this job's python 3.11
|
||||
# (see the Dockerfile's explorer-extra-py313.txt for why this
|
||||
# can't be shared with python 3.13: audioread needs extra
|
||||
# standard-aifc/standard-sunau hashes only on 3.13+).
|
||||
#
|
||||
# --no-deps only skips *runtime* dependency resolution - `-e .`
|
||||
# still does a PEP 517 build, which by default creates an isolated
|
||||
# build env and fetches [build-system] requires (setuptools,
|
||||
# wheel) completely outside any hash checking. Install
|
||||
# pep517-build.txt (pins that exact build-system.requires) first
|
||||
# and pass --no-build-isolation so pip reuses those hash-verified
|
||||
# copies instead of fetching its own.
|
||||
pip install -r .github/requirements/pep517-build.txt --require-hashes
|
||||
pip install --no-deps --no-build-isolation -e .
|
||||
pip install -r .github/requirements/explorer-extra-py311.txt --require-hashes
|
||||
pip install -r .github/requirements/pytest-tool.txt --require-hashes
|
||||
- name: Test deterministic Explorer backend path
|
||||
run: |
|
||||
pytest -q tests/explorer/test_explorer_deterministic_rendering_e2e.py
|
||||
- name: Install pinned Python dependencies
|
||||
run: |
|
||||
pip install -r requirements-ci.txt
|
||||
pip install -r requirements-ci.txt --require-hashes
|
||||
- name: Verify requirements-ci.txt is up to date
|
||||
run: |
|
||||
pip install uv==0.12.1
|
||||
pip install -r .github/requirements/uv-tool.txt --require-hashes
|
||||
# Re-resolve with the committed file as a constraint: upstream package
|
||||
# releases must NOT fail CI (deps only change when pyproject.toml
|
||||
# changes intentionally). Compare only version lines (pkg==ver),
|
||||
@@ -72,10 +95,10 @@ jobs:
|
||||
diff \
|
||||
<(grep -E '^[a-zA-Z0-9._-]+==' requirements-ci.txt | sed 's/ \\$//') \
|
||||
<(grep -E '^[a-zA-Z0-9._-]+==' /tmp/requirements-ci-check.txt)
|
||||
- run: pip install build
|
||||
# wheel is build-time only (not in requirements-ci.txt) — install the
|
||||
# same pinned version [build-system] declares so --no-isolation works.
|
||||
- run: pip install wheel==0.48.0
|
||||
# build is a dev-time dependency; wheel is build-time only (neither is
|
||||
# in requirements-ci.txt) — install the same pinned versions
|
||||
# [build-system] declares so --no-isolation works below.
|
||||
- run: pip install -r .github/requirements/build-tools.txt --require-hashes
|
||||
- name: Build package (no isolation — pinned deps)
|
||||
run: python -m build --no-isolation
|
||||
- name: Verify Explorer frontend is packaged
|
||||
|
||||
@@ -10,13 +10,15 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze Python
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # for github/codeql-action/upload-sarif below
|
||||
actions: read # for github/codeql-action/init's CodeQL bundle cache lookup
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Container Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Mirrors .dockerignore's opt-in list exactly - anything not listed there
|
||||
# can't reach the build context, so it can't change the built image.
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- '.dockerignore'
|
||||
- 'pyproject.toml'
|
||||
- 'README.md'
|
||||
- 'LICENSE'
|
||||
- 'MANIFEST.in'
|
||||
- '.github/requirements/explorer-extra-py313.txt'
|
||||
- '.github/requirements/pep517-build.txt'
|
||||
- 'semantica/**'
|
||||
- 'integrations/**'
|
||||
- 'explorer/**'
|
||||
- '.github/workflows/container-scan.yml'
|
||||
schedule:
|
||||
- cron: '30 2 * * 1' # weekly, catches new CVEs published against the base image between pushes
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # for github/codeql-action/upload-sarif below
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t semantica:scan .
|
||||
|
||||
# Run Trivy as a digest-pinned image rather than the aquasecurity/trivy-action
|
||||
# marketplace wrapper: the aquasecurity GitHub org has an IP allow list on its
|
||||
# API that 403s verify-action-pins.sh's live tag->SHA check from Actions-runner
|
||||
# IPs, and this repo already treats Trivy's action pin as a known past target
|
||||
# for tag-repointing (see the LiteLLM/Trivy 2026 incident note above). Pulling
|
||||
# by sha256 digest from Docker Hub is immutable and verifiable independently of
|
||||
# GitHub's API, so it sidesteps both problems at once instead of carving a skip
|
||||
# exception into the pin verifier for an org already flagged as higher-risk.
|
||||
#
|
||||
# Report-only for now: this is Trivy's first run against this image, so we
|
||||
# don't yet know the CRITICAL/HIGH baseline. Findings still land in the
|
||||
# Security tab either way. Once triaged, add `--exit-code 1` (like
|
||||
# Safety/Bandit-HIGH in security-scan.yml) to make it a hard gate.
|
||||
- name: Scan image for vulnerabilities (Trivy)
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "$PWD:/output" \
|
||||
aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 \
|
||||
image --format sarif --output /output/trivy-results.sarif \
|
||||
--severity CRITICAL,HIGH --ignore-unfixed semantica:scan
|
||||
|
||||
- name: Upload Trivy SARIF
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-container
|
||||
|
||||
- name: Generate SBOM (Syft)
|
||||
if: always()
|
||||
uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
|
||||
with:
|
||||
image: semantica:scan
|
||||
format: spdx-json
|
||||
output-file: semantica-sbom.spdx.json
|
||||
@@ -28,12 +28,14 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
MSDO:
|
||||
# currently only windows-latest is supported
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # for github/codeql-action/upload-sarif below
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -66,7 +68,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Checkov
|
||||
run: python -m pip install checkov==3.3.1
|
||||
run: pip install -r .github/requirements/checkov.txt --require-hashes
|
||||
|
||||
- name: Run Checkov
|
||||
shell: pwsh
|
||||
@@ -74,12 +76,28 @@ jobs:
|
||||
PYTHONUTF8: "1"
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force reports | Out-Null
|
||||
checkov --directory . --framework kubernetes helm dockerfile github_actions secrets bicep arm --soft-fail --output sarif --output-file-path reports/checkov.sarif
|
||||
if (-not (Test-Path reports/checkov.sarif)) {
|
||||
checkov --directory . --framework kubernetes helm dockerfile github_actions secrets bicep arm --soft-fail --output sarif --output json --output-file-path reports
|
||||
if (-not (Test-Path reports/results_sarif.sarif)) {
|
||||
$sarif = Get-ChildItem -Path reports -Recurse -Filter *.sarif | Select-Object -First 1
|
||||
if ($null -eq $sarif) { throw "Checkov did not produce a SARIF file" }
|
||||
Copy-Item $sarif.FullName reports/checkov.sarif
|
||||
Copy-Item $sarif.FullName reports/results_sarif.sarif
|
||||
}
|
||||
if (-not (Test-Path reports/results_json.json)) {
|
||||
$json = Get-ChildItem -Path reports -Recurse -Filter *.json | Select-Object -First 1
|
||||
if ($null -eq $json) { throw "Checkov did not produce a JSON file" }
|
||||
Copy-Item $json.FullName reports/results_json.json
|
||||
}
|
||||
|
||||
# checkov's SARIF exporter includes checks it internally marked SKIPPED
|
||||
# (via the inline `# checkov:skip=` comments / `checkov.io/skipN`
|
||||
# annotations already on the Helm chart) as ordinary un-suppressed
|
||||
# results - it never uses SARIF's own `suppressions` field, so GitHub
|
||||
# opens a fresh alert for the same already-suppressed finding on every
|
||||
# single run (see #6035/#6036, #6112-6115, #6128-6131). checkov's JSON
|
||||
# output does correctly record the skip, so cross-reference it here
|
||||
# instead of re-dismissing the same alerts by hand forever.
|
||||
- name: Filter checkov's own suppressed checks out of the SARIF
|
||||
run: python .github/scripts/filter_checkov_skipped.py reports/results_json.json reports/results_sarif.sarif reports/checkov.sarif
|
||||
|
||||
- name: Upload Checkov results to Security tab
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Install Matrix
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # weekly, catches upstream dependency breakage between releases
|
||||
workflow_run:
|
||||
# The Release workflow publishes the GitHub release *before* it uploads to
|
||||
# PyPI (see release.yml), so triggering on `release: published` would race
|
||||
# the PyPI upload and could pass by silently installing the prior version.
|
||||
# workflow_run fires only after the whole Release workflow - including the
|
||||
# PyPI publish step - has finished.
|
||||
workflows: ['Release']
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
verify-install:
|
||||
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
||||
name: pip install semantica (${{ matrix.os }}, py${{ matrix.python-version }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12']
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- name: Pin expected version for release-triggered runs
|
||||
id: expected-version
|
||||
if: github.event_name == 'workflow_run'
|
||||
shell: bash
|
||||
env:
|
||||
EXPECTED_TAG: ${{ github.event.workflow_run.head_branch }}
|
||||
run: |
|
||||
expected="${EXPECTED_TAG#v}"
|
||||
if [ -z "$expected" ]; then
|
||||
echo "::error::Could not determine a release tag from the triggering workflow run (head_branch was empty)."
|
||||
exit 1
|
||||
fi
|
||||
echo "constraint===$expected" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- id: setup-semantica
|
||||
uses: ./.github/actions/setup-semantica
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
version: ${{ steps.expected-version.outputs.constraint }}
|
||||
|
||||
- name: Smoke test import
|
||||
shell: bash
|
||||
run: |
|
||||
python -c "
|
||||
import semantica
|
||||
print('semantica', semantica.__version__, 'installed and importable')
|
||||
"
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write # for the GitHub Release
|
||||
id-token: write # for PyPI Trusted Publishing (OIDC) and attestation signing
|
||||
id-token: write # for PyPI Trusted Publishing (OIDC), attestation signing, and Sigstore
|
||||
attestations: write # for SLSA build provenance
|
||||
# If you add another job to this workflow, give it its own explicit
|
||||
# `permissions:` block rather than relying on the workflow-level default
|
||||
@@ -39,11 +39,11 @@ jobs:
|
||||
# Install the pinned dependency set (with hashes) so the sdist/wheel
|
||||
# build runs against the same versions CI tests against.
|
||||
- name: Install pinned build dependencies
|
||||
run: pip install -r requirements-ci.txt
|
||||
- run: pip install build
|
||||
# wheel is build-time only (not in requirements-ci.txt) — install the
|
||||
# same pinned version [build-system] declares so --no-isolation works.
|
||||
- run: pip install wheel==0.48.0
|
||||
run: pip install -r requirements-ci.txt --require-hashes
|
||||
# build is a dev-time dependency; wheel is build-time only (neither is
|
||||
# in requirements-ci.txt) — install the same pinned versions
|
||||
# [build-system] declares so --no-isolation works below.
|
||||
- run: pip install -r .github/requirements/build-tools.txt --require-hashes
|
||||
- name: Build package (no isolation — pinned deps)
|
||||
run: python -m build --no-isolation
|
||||
- name: Verify Explorer frontend is packaged
|
||||
@@ -63,11 +63,29 @@ jobs:
|
||||
|
||||
print("Explorer frontend is packaged")
|
||||
PY
|
||||
- name: Verify PyPI long-description will render
|
||||
run: |
|
||||
pip install -r .github/requirements/twine.txt --require-hashes
|
||||
twine check dist/*
|
||||
- name: Attest build provenance
|
||||
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
|
||||
with:
|
||||
subject-path: 'dist/*'
|
||||
- uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
|
||||
# attest-build-provenance publishes to the GH attestations API only, which
|
||||
# OpenSSF Scorecard's Signed-Releases check does not inspect - it looks for
|
||||
# signature files attached as release assets. Sign here too so
|
||||
# `dist/*.sigstore.json` bundles ship alongside the wheel/sdist on the
|
||||
# GitHub Release itself.
|
||||
- name: Sign artifacts with Sigstore
|
||||
uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
|
||||
with:
|
||||
files: dist/*
|
||||
inputs: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
- uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
dist/*.sigstore.json
|
||||
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Scorecard supply-chain security
|
||||
|
||||
permissions: read-all
|
||||
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '30 1 * * 6' # weekly
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # to upload SARIF results
|
||||
id-token: write # to publish results and get a badge
|
||||
contents: read
|
||||
actions: read # to detect GitHub Actions workflows
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -44,15 +44,15 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r .github/requirements/bootstrap.txt --require-hashes
|
||||
# Install the pinned dependency set FIRST so Safety scans Semantica's
|
||||
# exact CI/release dependency tree (requirements-ci.txt is generated
|
||||
# from pyproject.toml extras, so this covers the project's real deps).
|
||||
pip install -r requirements-ci.txt
|
||||
pip install -r requirements-ci.txt --require-hashes
|
||||
# Tooling AFTER the pinned set: installing safety/bandit/semgrep/jq
|
||||
# first lets the pinned requirements overwrite their transitive deps
|
||||
# (e.g. rich), which breaks the safety CLI at runtime.
|
||||
pip install safety bandit semgrep jq
|
||||
pip install -r .github/requirements/security-scan-tools.txt --require-hashes
|
||||
|
||||
- name: Run Safety Check (Package Vulnerabilities)
|
||||
run: |
|
||||
|
||||
@@ -25,18 +25,18 @@ jobs:
|
||||
# Upgrade first: actions/setup-python's baked-in setuptools has been
|
||||
# behind known-vulnerable floors before (e.g. PYSEC-2026-3447 /
|
||||
# setuptools 75.1.0), so don't trust the preinstalled one.
|
||||
- run: python -m pip install --upgrade pip setuptools
|
||||
- run: pip install -r .github/requirements/bootstrap.txt --require-hashes
|
||||
# Audit the pinned dependency set (requirements-ci.txt is compiled from
|
||||
# pyproject.toml with --extra all — the same coverage as the [all]
|
||||
# extra, minus the Linux-only gpu set — so this keeps scan parity with
|
||||
# CI/release builds without a time-dependent resolution). This is the
|
||||
# fix for PYSEC-2024-38 (#869): the bare-env job never had fastapi or
|
||||
# python-multipart installed to look at.
|
||||
- run: pip install -r requirements-ci.txt
|
||||
- run: pip install -r requirements-ci.txt --require-hashes
|
||||
# PR runs gate on findings, since they're scoped to actual
|
||||
# pyproject.toml changes under review. The schedule/workflow_dispatch
|
||||
# runs stay non-blocking until a full pass over pre-existing findings
|
||||
# across the whole [all] tree has been done.
|
||||
- run: pip install pip-audit
|
||||
- run: pip install -r .github/requirements/pip-audit.txt --require-hashes
|
||||
- run: pip-audit -r requirements-ci.txt
|
||||
continue-on-error: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -213,6 +213,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **RETE engine matched every fact against every rule — `AlphaNode._matches()` and `BetaNode._can_join()` were placeholder stubs that always returned `True`** (closes #300)
|
||||
- `semantica/reasoning/rete_engine.py` shipped a Rete network whose per-condition alpha test and cross-condition beta join were both `return True` stubs, so `match_patterns()` fired every rule for every fact regardless of predicate, arity, or shared-variable consistency
|
||||
- New module-level `unify_condition()` reuses the regex-based approach from `Reasoner._match_pattern()`: a condition pattern like `Person(?x)` / `Parent(?x, ?y)` is compiled against a fact's `predicate(arg, ...)` string, `?var` becomes a named capture group, and a variable seen twice within one condition (e.g. `Loves(?x, ?x)`) becomes a backreference, so it only unifies when both positions hold the same value. Returns the bindings dict or `None`
|
||||
- Reworked propagation to carry partial-match **tokens** instead of bare facts: a new `Token` dataclass bundles the accumulated `facts` with the consistent `bindings`. `AlphaNode` emits a single-fact token per match; `BetaNode.join()` merges a left token with a right token, concatenating their facts in condition order and returning the merged token only when shared variables agree (conflicting values → `None`, no join). Terminal activations carry the full fact list and accumulated bindings through to the emitted match
|
||||
- This fixes a P1 chained-join defect: rules with three or more conditions (e.g. `Person(?x)`, `Parent(?x, ?y)`, `Located(?y, ?z)`) previously lost bindings and accumulated wrong facts at the third join, and a conflicting third condition could spuriously fire. Beta nodes now keep both `left_tokens` and `right_tokens` memories and join each new token against every token on the opposite side, so deep chains stay binding-consistent and third-level conflicts are correctly suppressed
|
||||
- Fixed an adjacent network-topology bug surfaced by the above: newly created beta nodes were never appended to their input nodes' `children`, so tokens could not propagate; propagation was reworked to support chained joins and to thread bindings end-to-end
|
||||
- Reconciled with the rule-actions/provenance layer (#1096) merged after this fix was opened: `execute_matches()` still dedupes and fires `Rule.actions`/legacy `handler` through a bound `Reasoner` via `_make_activation_key`, now sourced from the Token model's own `bindings` instead of the interim `_bindings_for_rule()` regex re-extraction, which is removed as redundant
|
||||
- New `tests/reasoning/test_rete_engine.py`: `unify_condition` unit cases (single/multi variable, literal args, predicate mismatch, repeated-variable equality), alpha match/reject, beta consistent-join vs conflict-reject, end-to-end rules (single-condition fires only the matching fact; multi-condition join fires only on consistent bindings), and a `TestThreeConditionChain` suite (valid three-condition match, third-level conflict suppression, insertion-order independence, `Match.facts` complete and in condition order, multiple left tokens joining one right fact, parity against `Reasoner._match_rule()`, and `reset()` clearing all token memory)
|
||||
|
||||
- **KG provenance tests asserted on generated ID strings instead of stored records, and `kg_provenance.py` was missed by the `utcnow` sweep** (closes #946) by @pravit-amp
|
||||
- The KG workflow and integration suites checked that a tracker call returned an ID matching a prefix (`assert cent_id.startswith("centrality_")`) without ever reading the record back, so an ID generator that returned a well-formed string and wrote nothing would have passed. Worse, some of those calls named tracker methods that do not exist anywhere in `semantica/` (`track_layer_analysis`, `track_centrality_score`), so the assertions were satisfied with no real interaction behind them
|
||||
- Those tests now read provenance back through `get_provenance()` and assert on algorithm metadata, and call the methods that actually persist records. Verified by mutation rather than by a green run alone: neutering the manager's storage write (`self.storage.store(...)` → no-op) fails 10 tests
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
cff-version: 1.2.0
|
||||
message: "If you use this software, please cite it as below."
|
||||
title: "Semantica: Graph-Native Infrastructure for Context and Accountable AI Systems"
|
||||
type: software
|
||||
authors:
|
||||
- name: "Semantica"
|
||||
repository-code: "https://github.com/semantica-agi/semantica"
|
||||
url: "https://getsemantica.ai"
|
||||
license: MIT
|
||||
version: 0.6.7
|
||||
date-released: 2026-08-28
|
||||
keywords:
|
||||
- knowledge-graph
|
||||
- context-graph
|
||||
- ai-agents
|
||||
- llm
|
||||
- decision-intelligence
|
||||
- provenance
|
||||
- explainability
|
||||
- graph-rag
|
||||
+38
-4
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:26-alpine AS frontend-builder
|
||||
FROM node:26-alpine@sha256:2d984a15c9b54fd0aeb608b8e0d0d83529eb34d2966db27a1fb4f1edc3d298a3 AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY explorer/package*.json ./explorer/
|
||||
@@ -9,7 +9,18 @@ RUN npm ci
|
||||
COPY explorer/ ./
|
||||
RUN mkdir -p /app/semantica && npm run build
|
||||
|
||||
FROM python:3.13-slim AS runtime
|
||||
# CVE-2026-14456 (OpenSSL QUIC-server DoS, flagged against this base image's
|
||||
# openssl/libssl3t64/openssl-provider-legacy): the Debian fix
|
||||
# (3.5.7-1~deb13u2) is only in trixie-proposed-updates as of this writing,
|
||||
# not yet promoted to trixie-security, so there's no package to pin here
|
||||
# today. Deliberately NOT running `apt-get upgrade` to chase it - that
|
||||
# breaks build reproducibility (terrascan AC_DOCKER_0052) and still
|
||||
# wouldn't reach a proposed-updates-only package. Once Debian ships the fix
|
||||
# and rebuilds this tag, the docker Dependabot ecosystem in
|
||||
# .github/dependabot.yml opens a PR bumping the digest pin above. Also: this
|
||||
# image only serves plain HTTP via uvicorn and never opens a QUIC listener,
|
||||
# so the bug isn't reachable here regardless.
|
||||
FROM python:3.13-slim@sha256:7ce4b6dfe35e55397b7cda544f8a13f191b7ae28dc5aad71fe664dbc9bc2623f AS runtime
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
@@ -22,12 +33,35 @@ WORKDIR /app
|
||||
RUN groupadd --system semantica \
|
||||
&& useradd --system --gid semantica --home-dir /app --shell /usr/sbin/nologin semantica
|
||||
|
||||
COPY pyproject.toml README.md LICENSE MANIFEST.in ./
|
||||
COPY pyproject.toml README.md LICENSE MANIFEST.in \
|
||||
.github/requirements/explorer-extra-py313.txt .github/requirements/pep517-build.txt ./
|
||||
COPY semantica/ ./semantica/
|
||||
COPY integrations/ ./integrations/
|
||||
COPY --from=frontend-builder /app/semantica/static ./semantica/static
|
||||
|
||||
RUN pip install --no-cache-dir ".[explorer]" \
|
||||
# explorer-extra-py313.txt is `uv pip compile pyproject.toml --extra explorer
|
||||
# --python-version 3.13 --constraint requirements-ci.txt --generate-hashes`
|
||||
# (see ci.yml's explorer-extra-py311.txt for the CI counterpart, resolved
|
||||
# for CI's python 3.11 instead - the two aren't interchangeable: audioread
|
||||
# (via librosa) needs standard-aifc/standard-sunau only on python>=3.13,
|
||||
# since aifc/sunau left stdlib there, so a 3.11-resolved lockfile is
|
||||
# missing hashes pip needs on this image's actual 3.13 interpreter and
|
||||
# --require-hashes fails outright rather than silently under-pinning).
|
||||
# Every fetched package is hash-verified (Scorecard Pinned-Dependencies)
|
||||
# and pinned to the same versions CI audited, e.g. msgpack==1.2.1 and
|
||||
# setuptools==84.0.0 (which also replaces the base image's vulnerable
|
||||
# 70.3.0, CVE-2025-47273 - nothing else in the tree pulls a newer copy).
|
||||
# --no-deps on the local package itself: it's our own source tree, not a
|
||||
# fetch, so there's nothing to hash-pin there - but `pip install .` still
|
||||
# does a PEP 517 build, which by default creates an *isolated* build env
|
||||
# and fetches [build-system] requires (setuptools, wheel) completely
|
||||
# outside any hash checking. pep517-build.txt pins that exact
|
||||
# build-system.requires; installing it first and passing
|
||||
# --no-build-isolation makes pip reuse those hash-verified copies instead
|
||||
# of fetching its own.
|
||||
RUN pip install --no-cache-dir -r explorer-extra-py313.txt -r pep517-build.txt --require-hashes \
|
||||
&& pip install --no-cache-dir --no-deps --no-build-isolation . \
|
||||
&& rm -f explorer-extra-py313.txt pep517-build.txt \
|
||||
&& chown -R semantica:semantica /app
|
||||
|
||||
USER semantica
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Growth & Distribution Playbook
|
||||
|
||||
North star: **10,000 developers who actually use Semantica in real projects**, not a raw PyPI download number. Downloads are a lagging indicator of distribution, not a target to optimize directly.
|
||||
|
||||
```
|
||||
GitHub stars → Website visitors → PyPI installs → Weekly active users → Production deployments → Enterprise customers
|
||||
```
|
||||
The last two matter far more than the download count.
|
||||
|
||||
## Guardrails — do not do this
|
||||
|
||||
- No fake/looping CI jobs that repeatedly `pip install semantica` purely to inflate the graph. It's detectable, it produces zero real users, and it damages credibility with anyone doing diligence (investors, enterprise buyers, security reviewers).
|
||||
- No package-splitting purely to multiply install counts — only split into `semantica-*` packages when there's a real architectural reason.
|
||||
- No meaningless Docker pulls or notebook launches with no real content behind them.
|
||||
- Every item below should get someone from "installed it" to "used it for something real." If a channel can't do that, it's not worth building.
|
||||
|
||||
## 30-day priority sprint
|
||||
|
||||
Ordered by leverage-to-effort ratio; do these first.
|
||||
|
||||
| # | Initiative | Target |
|
||||
| - | ---------- | ------ |
|
||||
| 1 | ✅ GitHub Actions example + reusable `setup-semantica` composite action + install-matrix badge | done |
|
||||
| 2 | Google Colab notebooks | 10 |
|
||||
| 3 | Docker images (RAG, Graph, Agent, API) | 4-5 |
|
||||
| 4 | Hugging Face Spaces demos | 3-4 |
|
||||
| 5 | LangChain integration + example | 1 |
|
||||
| 6 | LlamaIndex integration + example | 1 |
|
||||
| 7 | Vector/graph DB integrations (Qdrant, Weaviate, Neo4j) | 3 |
|
||||
| 8 | MCP server + example | 1 (already have `mcp/` — package as a distributable example) |
|
||||
| 9 | Production-quality starter repos (FastAPI, Streamlit, Gradio) | 3 |
|
||||
| 10 | `awesome-rag` / `awesome-llm` / `awesome-knowledge-graph` list submissions | 3+ PRs |
|
||||
|
||||
Push everything through: GitHub → Discord (`sV34vps5hH`) → X (`@BuildSemantica`) → GitHub Discussions → Reddit → Hacker News → relevant newsletters.
|
||||
|
||||
## Full channel checklist
|
||||
|
||||
### CI/CD (highest-intent distribution — installs tied to real pipelines)
|
||||
|
||||
- [x] GitHub Actions example in `examples/ci/github-actions.yml`
|
||||
- [x] Reusable composite GitHub Action — [`.github/actions/setup-semantica`](.github/actions/setup-semantica/action.yml), modeled on `actions/setup-python`; usable by any repo as `uses: semantica-agi/semantica/.github/actions/setup-semantica@main`
|
||||
- [x] "pip install" status badge in the README, backed by [`.github/workflows/install-matrix.yml`](.github/workflows/install-matrix.yml) — verifies the *published* package installs cleanly on Ubuntu/macOS/Windows across Python 3.9-3.12, weekly + on every release
|
||||
- [x] GitLab CI template — `examples/ci/gitlab-ci.yml`
|
||||
- [x] CircleCI template — `examples/ci/circleci-config.yml`
|
||||
- [ ] Jenkins, Azure DevOps, Bitbucket Pipelines, Buildkite, Travis CI equivalents
|
||||
|
||||
### Release pipeline hardening (already had Trusted Publishing/OIDC + SLSA attestation — this rounds it out to match top-tier OSS release practice)
|
||||
|
||||
- [x] `twine check` gate in `.github/workflows/release.yml` before publish — catches a broken PyPI long-description render before it goes live instead of after (a malformed README on the live PyPI page is a silent conversion killer)
|
||||
- [x] `CITATION.cff` (see Academic & research below)
|
||||
- [x] OpenSSF Scorecard (see Discoverability below)
|
||||
- [ ] Considered and deliberately skipped: Release Drafter / auto-generated changelogs — this repo hand-curates `CHANGELOG.md` with far more detail (PR numbers, contributors, phase-1 limitations) than a bot would produce. Don't introduce this without checking with maintainers first.
|
||||
- [ ] Renovate / Dependabot config templates that auto-bump the `semantica` version in downstream repos — real recurring CI runs on real adopters
|
||||
- [ ] Nightly scheduled workflow template that tests a downstream project against `semantica@latest`
|
||||
|
||||
### Containers & dev environments
|
||||
|
||||
- [ ] Official Docker images: RAG, Graph, Agent, API, `+Postgres`, `+Neo4j`, `+Qdrant`
|
||||
- [ ] `docker-compose` examples (repo already has `docker-compose.dev.yml` / `docker-compose.yml` as a base)
|
||||
- [ ] `.devcontainer/devcontainer.json` for one-click "Reopen in Container"
|
||||
- [ ] GitHub Codespaces-ready config
|
||||
- [ ] Gitpod config
|
||||
- [ ] "Use this template" GitHub repo button so new projects start with `semantica` in `requirements.txt`
|
||||
|
||||
### Notebooks & hosted demos
|
||||
|
||||
- [ ] 10-20 Google Colab notebooks (Graph RAG, agent memory, entity resolution, semantic search, document intelligence)
|
||||
- [ ] Kaggle Notebooks/Kernels
|
||||
- [ ] Binder / mybinder.org config for instant repo launch
|
||||
- [ ] SageMaker Studio Lab / Databricks Community Edition / Paperspace Gradient examples
|
||||
- [ ] Hugging Face Spaces (Streamlit/Gradio) demos with `semantica` in `requirements.txt`
|
||||
- [ ] Public hosted playground (source on GitHub, install visible)
|
||||
|
||||
### Framework & data-store integrations
|
||||
|
||||
- [x] LangChain integration — `integrations/langchain/` (`SemanticaRetriever`, `SemanticaVectorStore`, `SemanticaKGTool`/`SemanticaDecisionTool`), `pip install semantica[langchain]`, shipped in 0.6.7
|
||||
- [ ] LlamaIndex integration + example
|
||||
- [ ] LangGraph example
|
||||
- [ ] Neo4j integration/example (docs already list it as a supported graph store — turn into a runnable example repo)
|
||||
- [ ] Vector DB examples: Qdrant, Weaviate, Milvus, Pinecone, Chroma, FAISS, pgvector, OpenSearch/Elasticsearch (FAISS/Pinecone/Weaviate/Qdrant/Milvus/PgVector already supported per `docs/community-projects.md` — package each as a standalone example)
|
||||
- [ ] LLM provider quickstarts: OpenAI, Anthropic, Gemini, Groq, Ollama, HuggingFace, DeepSeek, LiteLLM (already-supported providers per docs — each gets its own copy-paste quickstart)
|
||||
- [ ] CrewAI / Agno integration examples (already documented under `docs/integrations/`) — promote as standalone repos, not just docs pages
|
||||
|
||||
### Package managers & installers
|
||||
|
||||
- [ ] conda-forge feedstock
|
||||
- [ ] Homebrew formula for the CLI
|
||||
- [ ] Nix/nixpkgs packaging
|
||||
- [ ] Chocolatey / Scoop (Windows)
|
||||
- [ ] Document `uv add semantica` and `poetry add semantica` explicitly alongside `pip install`
|
||||
|
||||
### Downstream packages & CLI
|
||||
|
||||
- [ ] Genuinely useful `semantica-*` packages only where warranted (e.g. `semantica-rag`, `semantica-connectors`) — each pulls `semantica` as a real dependency
|
||||
- [ ] Make sure `semantica init / ingest / index / query / serve` CLI flows are the default onboarding path in every tutorial
|
||||
- [ ] VS Code extension wrapping the CLI (scaffold + run commands from the command palette)
|
||||
- [ ] JetBrains plugin equivalent
|
||||
|
||||
### Templates & starters
|
||||
|
||||
- [ ] Cookiecutter templates: `cookiecutter-semantic-rag`, `cookiecutter-ai-agent`, `cookiecutter-enterprise-rag`
|
||||
- [ ] Starter repos: FastAPI, Streamlit, Gradio, Next.js frontend + Semantica backend
|
||||
- [ ] Cloud deploy templates: AWS, GCP, Azure, Modal, Railway, Render, Fly.io (repo already has `deploy/azure`, `deploy/gcp`, `deploy/fly`, `deploy/railway`, `deploy/render`, `deploy/kubernetes`, `deploy/helm` — link these prominently from the README/quickstart, they're already-built distribution surface)
|
||||
- [ ] Terraform / Pulumi / Helm modules published to their respective registries
|
||||
|
||||
### Discoverability & curation
|
||||
|
||||
- [ ] Submit to `awesome-rag`, `awesome-llm`, `awesome-knowledge-graph`, `awesome-python`
|
||||
- [ ] Pitch newsletters with engaged Python/AI audiences (Python Weekly, Import AI, TLDR AI, etc.)
|
||||
- [x] PyPI trove classifiers/keywords and `project.urls` (Homepage/Docs/Repository/Changelog/Bug Tracker) — already complete in `pyproject.toml`
|
||||
- [ ] Get listed on Papers With Code for any retrieval/graph-RAG benchmark work
|
||||
- [x] [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/semantica-agi/semantica) badge + weekly workflow (`.github/workflows/scorecard.yml`) — a concrete trust signal security/procurement teams check before greenlighting adoption, which gates real (non-CI-bot) install growth at enterprises
|
||||
|
||||
### Academic & research
|
||||
|
||||
- [x] `CITATION.cff` at repo root — enables GitHub's native "Cite this repository" button, feeds Google Scholar/academic tooling; complements `docs/citation.md` (still needs a real Zenodo DOI to replace the `XXXXXXX` placeholder in both places once one is minted)
|
||||
- [ ] arXiv paper if there's real architectural novelty to describe
|
||||
- [ ] Zenodo DOI for citability (`docs/citation.md` already exists — make sure it points to a real DOI)
|
||||
- [ ] Workshop/tutorial sessions at PyData/ODSC-style events with hands-on install steps
|
||||
- [ ] University course material / bootcamp adoption outreach
|
||||
|
||||
### Content
|
||||
|
||||
- [ ] Reproducible benchmark repos (Graph RAG vs vector RAG, retrieval@k, enterprise-scale retrieval) with `pip install semantica && python benchmark.py`
|
||||
- [ ] 20-30 real-world example applications (RAG, enterprise document intelligence, financial entity graphs, code knowledge graphs, research discovery, agent memory)
|
||||
- [ ] Blog/tutorial posts on Dev.to, Medium, personal blogs — always with runnable code, not just prose
|
||||
- [ ] Contribute integrations/PRs to other projects building RAG/agents/knowledge graphs — "I implemented Semantica support" beats "please use Semantica"
|
||||
|
||||
## Tracking
|
||||
|
||||
Don't just watch the raw PyPI number — use download analytics (e.g. PePy) to separate CI/bot traffic from real installs, and track the funnel above end-to-end where possible (stars → site visits → installs → weekly actives).
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
> Ingest your enterprise data, extract what matters, build a Context Graph and knowledge graph (KG), and run graph analytics and causal reasoning over all of it, with full decision provenance baked in. Explainable, traceable, and trustworthy by design.
|
||||
|
||||
**Decision Intelligence · Context Management · Deterministic Reasoning · Ontology Management · Knowledge Modeling · End-to-End Traceability**
|
||||
**Context Management · Knowledge Modeling · Deterministic Reasoning · Ontology Management · Decision Intelligence · End-to-End Traceability**
|
||||
|
||||
**Open Source · Self-Hostable · Auditable · Governed · Zero Vendor Lock-In**
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#### Built for High-Stakes, Regulated Domains
|
||||
|
||||
[](https://github.com/semantica-agi/semantica) [](https://github.com/semantica-agi/semantica/network/members) [](https://github.com/semantica-agi/semantica/graphs/contributors) [](https://pypi.org/project/semantica/) [](https://pepy.tech/project/semantica) [](https://www.python.org/) [](https://opensource.org/licenses/MIT) [](https://github.com/semantica-agi/semantica/actions) [](https://deepwiki.com/semantica-agi/semantica)
|
||||
[](https://github.com/semantica-agi/semantica) [](https://github.com/semantica-agi/semantica/network/members) [](https://github.com/semantica-agi/semantica/graphs/contributors) [](https://pypi.org/project/semantica/) [](https://pepy.tech/project/semantica) [](https://www.python.org/) [](https://opensource.org/licenses/MIT) [](https://github.com/semantica-agi/semantica/actions) [](https://github.com/semantica-agi/semantica/actions/workflows/install-matrix.yml) [](https://scorecard.dev/viewer/?uri=github.com/semantica-agi/semantica) [](https://deepwiki.com/semantica-agi/semantica)
|
||||
|
||||
[](https://getsemantica.ai/) [](https://docs.getsemantica.ai/) [](https://discord.gg/sV34vps5hH) [](https://x.com/BuildSemantica) [](https://www.youtube.com/watch?v=QfnNZg4-dZA) [](CHANGELOG.md)
|
||||
|
||||
@@ -56,9 +56,7 @@ pip install semantica
|
||||
|
||||
---
|
||||
|
||||
Most AI agents act without a trail. They store embeddings, not meaning: context that can't be explained, decisions that can't be audited. In lending, that gap is a compliance exposure, not an inconvenience: an underwriting agent's approval has to survive a regulator's "why" months later.
|
||||
|
||||
Semantica sits underneath your LLM, vector store, and agent framework as a deterministic infrastructure layer: no LLM required for graph construction, reasoning, or provenance.
|
||||
Most AI agents run on embeddings, not meaning: similarity scores with no structure, no relationships, and no way to explain why a result came back. Semantica is the semantic/context layer underneath your LLM, vector store, and agent framework: a deterministic infrastructure layer (no LLM required for graph construction, reasoning, or provenance) that turns fragmented enterprise data into a structured, queryable Context Graph and knowledge graph, governed by ontologies and controlled vocabularies (OWL, SHACL, SKOS) so the meaning of your data is explicit, not just its embedding. Decision provenance and audit trails fall out of that structure as a property, not the product itself; in domains a regulator can question, that same structure just happens to double as a straight answer to "why."
|
||||
|
||||
> ⚠️ **System-level explainability, not foundation-model explainability.** Semantica does not expose or reconstruct what happens *inside* the LLM — its internal reasoning or chain-of-thought stays opaque, as it does for any external system. Semantica explains what's *outside* the model: the context and data fed in, the decision produced, its provenance, relevant relationships, applied policies, and the full execution trail.
|
||||
|
||||
@@ -279,7 +277,7 @@ retrieved = ctx.retrieve("who approved the Acme contract?")
|
||||
|
||||
## Recipe: Audit Trail for a Regulated Decision
|
||||
|
||||
The flagship pattern: record a causally-linked decision chain, attach provenance to every entity, and export a regulator-ready audit trail.
|
||||
One pattern built on the same Context Graph: record a causally-linked decision chain, attach provenance to every entity, and export a regulator-ready audit trail.
|
||||
|
||||
```python
|
||||
from semantica.context import ContextGraph
|
||||
@@ -1030,7 +1028,7 @@ team = Team(agents=[researcher, analyst], mode="coordinate")
|
||||
|
||||
## More Recipes
|
||||
|
||||
The flagship audit-trail recipe is [above](#recipe-audit-trail-for-a-regulated-decision). Here are three more common patterns.
|
||||
The audit-trail recipe is [above](#recipe-audit-trail-for-a-regulated-decision). Here are three more common patterns.
|
||||
|
||||
<details>
|
||||
<summary><b>End-to-End GraphRAG Pipeline</b></summary>
|
||||
@@ -1534,6 +1532,20 @@ git clone https://github.com/semantica-agi/semantica.git
|
||||
cd semantica && pip install -e ".[dev]" && pytest tests/
|
||||
```
|
||||
|
||||
### CI & Deployment
|
||||
|
||||
Wiring `semantica` into your own CI is a two-minute job. On GitHub Actions, use the reusable composite action:
|
||||
|
||||
```yaml
|
||||
- uses: semantica-agi/semantica/.github/actions/setup-semantica@main
|
||||
with:
|
||||
python-version: '3.11'
|
||||
```
|
||||
|
||||
Copy-paste starting templates for GitHub Actions, GitLab CI, and CircleCI live in [examples/ci/](examples/ci/). The published package itself is verified installable across Ubuntu/macOS/Windows and Python 3.9-3.12 every week by the [Install Matrix workflow](.github/workflows/install-matrix.yml).
|
||||
|
||||
Ready-made deployment configs for AWS, GCP, Azure, Fly.io, Railway, Render, Kubernetes, and Helm are in [deploy/](deploy/).
|
||||
|
||||
---
|
||||
|
||||
## Enterprise
|
||||
|
||||
+152
-24
@@ -28,6 +28,7 @@ The `semantica.llms` module provides a unified interface for connecting to Large
|
||||
## When To Use / When Not To Use
|
||||
|
||||
**Use LLM integrations for:**
|
||||
|
||||
- Text generation, summarization, and question-answering tasks
|
||||
- Complex reasoning that requires natural language understanding
|
||||
- Structured data extraction from unstructured text
|
||||
@@ -35,6 +36,7 @@ The `semantica.llms` module provides a unified interface for connecting to Large
|
||||
- Tasks where context, ambiguity, or domain knowledge matter
|
||||
|
||||
**Deterministic tools may be better for:**
|
||||
|
||||
- Pattern matching that regular expressions can handle
|
||||
- Simple rule-based classification with clear criteria
|
||||
- Mathematical calculations or statistical analysis
|
||||
@@ -42,6 +44,7 @@ The `semantica.llms` module provides a unified interface for connecting to Large
|
||||
- Data transformations with known logic
|
||||
|
||||
**A full LLM may be unnecessary for:**
|
||||
|
||||
- Simple keyword search or exact string matching
|
||||
- Deterministic workflows with predefined decision trees
|
||||
- High-frequency, low-latency operations where inference overhead matters
|
||||
@@ -59,7 +62,7 @@ Four factors drive provider selection, each optimized for different use cases:
|
||||
|
||||
**Accuracy** matters most in high-stakes decisions: clinical contraindication checks, credit committee reasoning, and legal document analysis. Frontier models like Claude or GPT-4 available through `LiteLLM` provide the strongest reasoning capabilities.
|
||||
|
||||
**Data residency** constraints eliminate cloud providers for classified or HIPAA-regulated workloads. `HuggingFaceLLM` with local model paths enables fully air-gapped deployments without network calls.
|
||||
**Data residency** constraints eliminate cloud providers for classified or HIPAA-regulated workloads. `HuggingFaceLLM` with local model paths, or `Ollama` pointed at a local server, both enable fully air-gapped deployments without network calls.
|
||||
|
||||
**Cost at scale** favors high-throughput providers like Novita AI for bulk extraction pipelines processing thousands of documents per hour where per-token costs accumulate quickly.
|
||||
|
||||
@@ -143,6 +146,131 @@ risk_data = oai.generate_structured(
|
||||
|
||||
The default model `gpt-3.5-turbo` is fine for classification and light extraction. Switch to `gpt-4o` for complex multi-step regulatory reasoning or document understanding.
|
||||
|
||||
## Anthropic — Complex Reasoning and Structured Extraction
|
||||
|
||||
**Anthropic** provides the Claude model family, built with an emphasis on careful, instruction-following behavior and strong performance on multi-step reasoning, long-document analysis, and code-related tasks. Claude models tend to be more cautious about ambiguous instructions than other providers. That matters when the cost of a confidently wrong answer is high.
|
||||
|
||||
The `Anthropic` provider wraps the Claude API. Reach for it when the task involves reasoning through several dependent steps (not just single-turn extraction), when you're processing long source documents that need to stay in context, or when you need schema-validated structured output rather than best-effort JSON.
|
||||
|
||||
Install with `pip install "semantica[llm-anthropic]"` (or just `pip install anthropic`) before using this provider.
|
||||
|
||||
```python
|
||||
from semantica.llms import Anthropic
|
||||
|
||||
claude = Anthropic(model="claude-sonnet-4-6", api_key="YOUR_ANTHROPIC_KEY")
|
||||
# api_key falls back to the ANTHROPIC_API_KEY environment variable
|
||||
|
||||
# is_available() only confirms a client was constructed from some key.
|
||||
# It does not validate the key or check network reachability - an
|
||||
# invalid or expired key still passes this check and fails at generate().
|
||||
if not claude.is_available():
|
||||
raise RuntimeError("Anthropic provider not configured - set ANTHROPIC_API_KEY")
|
||||
|
||||
# Plain generation - multi-step reasoning over a contract clause
|
||||
verdict = claude.generate(
|
||||
"A vendor contract has a 30-day termination-for-convenience clause "
|
||||
"but a 90-day data-return obligation that survives termination. "
|
||||
"If the customer terminates on day 1, when must vendor-held data "
|
||||
"be returned? Answer with the date basis only.",
|
||||
temperature=0.1,
|
||||
)
|
||||
print(verdict)
|
||||
# "Day 120 from termination notice. The 90-day return period runs from
|
||||
# the termination date (day 30), not from the notice date."
|
||||
|
||||
# Structured, schema-validated output
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ContractRisk(BaseModel):
|
||||
clause: str
|
||||
risk_level: str
|
||||
days_to_deadline: int
|
||||
|
||||
risk = claude.generate_typed(
|
||||
"Extract the termination clause risk from: vendor contract, "
|
||||
"30-day termination for convenience, 90-day post-termination "
|
||||
"data return obligation.",
|
||||
schema=ContractRisk,
|
||||
)
|
||||
print(risk.risk_level, risk.days_to_deadline)
|
||||
# "medium" 90
|
||||
```
|
||||
|
||||
Model selection follows the same tier structure as the other providers: a Haiku model for high-volume classification where cost matters more than depth, a Sonnet model as the default for most extraction and reasoning tasks, an Opus model when a task genuinely needs the deepest reasoning available and latency/cost are secondary. Check Anthropic's docs for the current model identifiers, since they're versioned and change over time.
|
||||
|
||||
## Gemini — Long Context and Multimodal Input
|
||||
|
||||
**Gemini** is Google's model family, with a context window large enough to hold entire codebases or long regulatory filings in a single call, and native support for image and document input alongside text. Reach for it when a task needs to reference a large amount of source material at once, or when the input isn't plain text.
|
||||
|
||||
The `Gemini` provider tries the newer `google-genai` SDK first and falls back to the older `google-generativeai` package if that's what's installed. Install with `pip install "semantica[llm-gemini]"` (or `pip install google-genai`) before using this provider.
|
||||
|
||||
```python
|
||||
from semantica.llms import Gemini
|
||||
|
||||
gemini = Gemini(model="gemini-pro", api_key="YOUR_GEMINI_KEY")
|
||||
# api_key falls back to the GEMINI_API_KEY environment variable
|
||||
|
||||
if not gemini.is_available():
|
||||
raise RuntimeError("Gemini provider not configured - set GEMINI_API_KEY")
|
||||
|
||||
response = gemini.generate(
|
||||
"Summarize the key obligations in a standard NDA in three bullet points."
|
||||
)
|
||||
print(response)
|
||||
|
||||
data = gemini.generate_structured(
|
||||
"Extract the party names and effective date from: "
|
||||
"This Agreement is entered into between Acme Corp and Globex LLC, "
|
||||
"effective January 1, 2026."
|
||||
)
|
||||
print(data)
|
||||
```
|
||||
|
||||
## Ollama — Local, Air-Gapped Inference
|
||||
|
||||
**Ollama** runs models entirely on your own machine, with no API key and no outbound network call. It's the right choice for air-gapped environments, offline development, or any workload where the source data can't leave the local network.
|
||||
|
||||
Unlike the other providers here, `Ollama` takes a `base_url` instead of an `api_key`. It talks to a local Ollama server over HTTP. Start the server with `ollama serve` and pull a model with `ollama pull llama2` before using this provider. Install the Python client with `pip install "semantica[llm-ollama]"` (or `pip install ollama`).
|
||||
|
||||
```python
|
||||
from semantica.llms import Ollama
|
||||
|
||||
llm = Ollama(model="llama2", base_url="http://localhost:11434")
|
||||
|
||||
if not llm.is_available():
|
||||
raise RuntimeError("Ollama provider not configured - is 'ollama serve' running?")
|
||||
|
||||
response = llm.generate("Explain the difference between a hash map and a tree map.")
|
||||
print(response)
|
||||
```
|
||||
|
||||
`is_available()` for Ollama does a real connectivity check (it calls the server's `list()` endpoint), unlike the API-key-based providers above, so a `False` here usually means the server isn't running rather than a missing credential.
|
||||
|
||||
## DeepSeek — Budget Reasoning at Scale
|
||||
|
||||
**DeepSeek** exposes an OpenAI-compatible API at a fraction of the cost of the larger US providers, with reasoning quality that holds up well for extraction and classification work. It's a reasonable default when you're processing a large volume of documents and don't need the deepest reasoning tier.
|
||||
|
||||
Install with `pip install "semantica[llm-deepseek]"` (or `pip install openai`, since DeepSeek is accessed through the OpenAI client pointed at a different base URL).
|
||||
|
||||
```python
|
||||
from semantica.llms import DeepSeek
|
||||
|
||||
llm = DeepSeek(model="deepseek-chat", api_key="YOUR_DEEPSEEK_KEY")
|
||||
# api_key falls back to the DEEPSEEK_API_KEY environment variable
|
||||
|
||||
if not llm.is_available():
|
||||
raise RuntimeError("DeepSeek provider not configured - set DEEPSEEK_API_KEY")
|
||||
|
||||
response = llm.generate("List three risks of using a floating IP in a Kubernetes ingress.")
|
||||
print(response)
|
||||
|
||||
data = llm.generate_structured(
|
||||
"Extract the CVE ID and affected product from: "
|
||||
"CVE-2024-3400 affects PAN-OS GlobalProtect gateways."
|
||||
)
|
||||
print(data)
|
||||
```
|
||||
|
||||
## LiteLLM — One Interface, 100+ Providers
|
||||
|
||||
**LiteLLM** is a universal adapter that provides a single interface to over 100 different LLM providers, including Anthropic Claude, Azure OpenAI, AWS Bedrock, Google Vertex AI, and local Ollama instances. It acts as a translation layer, converting your unified API calls into provider-specific requests, enabling easy switching between providers without code changes.
|
||||
@@ -306,30 +434,32 @@ for t in triplets:
|
||||
|
||||
## Novita AI — Cost-Efficient Bulk Extraction
|
||||
|
||||
Novita AI exposes an OpenAI-compatible API and is available as a built-in provider for the extraction layer. It is accessed differently from the `semantica.llms` classes — through `create_provider` from `semantica.semantic_extract.providers` — making it the right choice for high-volume NER pipelines where per-call cost matters.
|
||||
**Novita AI** exposes an OpenAI-compatible API at low per-call cost, making it a reasonable choice for high-volume NER pipelines where cost matters more than getting the single best answer.
|
||||
|
||||
Install with `pip install "semantica[llm-novita]"` (or `pip install openai`, since Novita is accessed through the OpenAI client pointed at a different base URL).
|
||||
|
||||
```python
|
||||
from semantica.llms import Novita
|
||||
|
||||
llm = Novita(model="deepseek/deepseek-v3.2", api_key="YOUR_NOVITA_KEY")
|
||||
# api_key falls back to the NOVITA_API_KEY environment variable
|
||||
|
||||
if not llm.is_available():
|
||||
raise RuntimeError("Novita provider not configured - set NOVITA_API_KEY")
|
||||
|
||||
response = llm.generate("Summarize the Basel III leverage ratio requirement.")
|
||||
|
||||
data = llm.generate_structured(
|
||||
"Extract drug names and dosages from: "
|
||||
"Patient received warfarin 5mg daily, aspirin 75mg daily, metformin 500mg twice daily."
|
||||
)
|
||||
```
|
||||
|
||||
Novita is also reachable as a provider name string for the NER interface, without going through the `Novita` class directly:
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract.providers import create_provider
|
||||
from semantica.semantic_extract import NamedEntityRecognizer
|
||||
|
||||
# create_provider pools instances — same key reuses the same object
|
||||
provider = create_provider(
|
||||
"novita",
|
||||
api_key="YOUR_NOVITA_KEY", # or set NOVITA_API_KEY env var
|
||||
model="deepseek/deepseek-v3.2", # default model
|
||||
)
|
||||
|
||||
if provider.is_available():
|
||||
# Plain generation
|
||||
response = provider.generate("Summarise the Basel III leverage ratio requirement.")
|
||||
|
||||
# Structured extraction — returns parsed dict
|
||||
data = provider.generate_structured(
|
||||
"Extract drug names and dosages from: "
|
||||
"Patient received warfarin 5mg daily, aspirin 75mg daily, metformin 500mg twice daily."
|
||||
)
|
||||
|
||||
# Use Novita through the NER interface — provider name as string
|
||||
ner = NamedEntityRecognizer(
|
||||
methods=["llm"],
|
||||
provider="novita",
|
||||
@@ -339,11 +469,9 @@ entities = ner.extract_entities(
|
||||
"CVE-2024-3400 is exploited by UNC3886 targeting PAN-OS GlobalProtect."
|
||||
)
|
||||
for e in entities:
|
||||
print("{} ({}) — conf={:.2f}".format(e.text, e.label, e.confidence))
|
||||
print("{} ({}) conf={:.2f}".format(e.text, e.label, e.confidence))
|
||||
```
|
||||
|
||||
Novita requires the `openai` Python client under the hood — install with `pip install "semantica[llm-openai]"` or `pip install openai`.
|
||||
|
||||
## Domain Examples
|
||||
|
||||
<Tabs>
|
||||
|
||||
+3
-2
@@ -327,10 +327,11 @@ print(f"Relationships active in 2023: {result_2023['num_relationships']}")
|
||||
<Accordion title="Persistent graph store: Neo4j, FalkorDB, Apache AGE" icon="database">
|
||||
|
||||
```python
|
||||
from semantica.graph_store import Neo4jStore
|
||||
from semantica.graph_store import GraphStore
|
||||
from semantica.kg import GraphBuilder
|
||||
|
||||
store = Neo4jStore(
|
||||
store = GraphStore(
|
||||
backend="neo4j",
|
||||
uri="bolt://localhost:7687",
|
||||
user="neo4j",
|
||||
password="password",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# CI templates
|
||||
|
||||
Copy-paste starting points for wiring `semantica` into your own project's CI. Each file is a
|
||||
complete, working config — rename it into your project (see the comment at the top of each file
|
||||
for the target path) and swap the smoke-test / test step for whatever your project does with
|
||||
Semantica. Each template installs `semantica` unconditionally and your own project's dependencies
|
||||
only if a `requirements.txt` is present; if your project uses `pyproject.toml`, Poetry, or Pipenv
|
||||
instead, adjust the marked install line (each file calls it out inline).
|
||||
|
||||
| File | Target path in your repo |
|
||||
| ---- | ------------------------- |
|
||||
| [`github-actions.yml`](github-actions.yml) | `.github/workflows/semantica.yml` |
|
||||
| [`gitlab-ci.yml`](gitlab-ci.yml) | `.gitlab-ci.yml` |
|
||||
| [`circleci-config.yml`](circleci-config.yml) | `.circleci/config.yml` |
|
||||
|
||||
If your own project is hosted on GitHub, you can skip the setup boilerplate entirely and use
|
||||
Semantica's reusable composite action instead:
|
||||
|
||||
```yaml
|
||||
- uses: semantica-agi/semantica/.github/actions/setup-semantica@main
|
||||
with:
|
||||
python-version: '3.11'
|
||||
# extras: 'explorer,all' # optional
|
||||
# version: '==0.6.7' # optional, pin an exact release
|
||||
# cache: 'pip' # optional, only if your repo has a requirements.txt/pyproject.toml/etc.
|
||||
```
|
||||
|
||||
`@main` always tracks this repo's default branch, which is convenient but — like any mutable
|
||||
ref — can change out from under you between runs. For production CI, pin it to a commit SHA
|
||||
instead (find one via `git rev-parse` against a tagged release, or the commit history for
|
||||
[`.github/actions/setup-semantica/`](../../.github/actions/setup-semantica/)) and update the pin
|
||||
deliberately when you want to pick up changes, the same way this repo's own workflows are pinned
|
||||
(see [`verify-action-pins.yml`](../../.github/workflows/verify-action-pins.yml)).
|
||||
|
||||
It installs Python, installs `semantica`, and verifies the import (pip caching is opt-in via `cache: 'pip'`, since not every caller repo has a requirements file to key the cache on) — see
|
||||
[`.github/actions/setup-semantica/action.yml`](../../.github/actions/setup-semantica/action.yml).
|
||||
@@ -0,0 +1,40 @@
|
||||
# Drop this in as .circleci/config.yml in your own project.
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: cimg/python:3.11
|
||||
steps:
|
||||
- checkout
|
||||
# A content-hashed cache key (e.g. `{{ checksum "requirements.txt" }}`)
|
||||
# is more precise but breaks if that exact file doesn't exist in your
|
||||
# project - swap in one matched to however you declare dependencies
|
||||
# once you've adjusted the install step below.
|
||||
- restore_cache:
|
||||
keys:
|
||||
- pip-cache-v1
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
pip install --upgrade pip
|
||||
pip install semantica
|
||||
# Install your own project's dependencies however your project
|
||||
# declares them - adjust this to match, e.g. `pip install -e .`
|
||||
# for pyproject.toml / setup.cfg, or `poetry install`.
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- save_cache:
|
||||
key: pip-cache-v1
|
||||
paths:
|
||||
- ~/.cache/pip
|
||||
- run:
|
||||
name: Smoke test
|
||||
command: python -c "import semantica; print('semantica', semantica.__version__)"
|
||||
- run:
|
||||
name: Run tests
|
||||
command: pytest
|
||||
|
||||
workflows:
|
||||
test:
|
||||
jobs:
|
||||
- test
|
||||
@@ -0,0 +1,44 @@
|
||||
# Drop this in as .github/workflows/semantica.yml in your own project.
|
||||
#
|
||||
# Installs Semantica and runs a smoke import + your test suite. Swap the
|
||||
# smoke-test step for whatever your project actually does with Semantica
|
||||
# (build a context graph, run an ingest pipeline, etc.).
|
||||
#
|
||||
# Third-party actions below are pinned to a commit SHA rather than a mutable
|
||||
# tag - a moved tag can silently swap in different code. Update the pin (and
|
||||
# the trailing "# vX" comment) deliberately when you want a newer version;
|
||||
# see semantica-agi/semantica's own .github/workflows/verify-action-pins.yml
|
||||
# for one way to keep pins honest automatically.
|
||||
name: Semantica
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install semantica
|
||||
# Install your own project's dependencies however your project
|
||||
# declares them - adjust this to match. Examples:
|
||||
# pip install -r requirements.txt
|
||||
# pip install -e . # pyproject.toml / setup.cfg
|
||||
# pip install -e ".[dev]"
|
||||
# poetry install
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Run tests
|
||||
run: pytest
|
||||
@@ -0,0 +1,20 @@
|
||||
# Drop this in as .gitlab-ci.yml in your own project.
|
||||
semantica-test:
|
||||
image: python:3.11-slim
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
script:
|
||||
- pip install --upgrade pip
|
||||
- pip install semantica
|
||||
# Install your own project's dependencies however your project declares
|
||||
# them - adjust this to match, e.g. `pip install -e .` for pyproject.toml
|
||||
# / setup.cfg, or `poetry install`.
|
||||
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- python -c "import semantica; print('semantica', semantica.__version__)"
|
||||
- pytest
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
Generated
+6
-6
@@ -2083,9 +2083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4250,9 +4250,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -318,6 +318,20 @@ interface EdgeListResponse {
|
||||
|
||||
const PAGE_LIMIT = 1000;
|
||||
|
||||
/** Surface the server's `detail` message (e.g. auth/setup guidance) on non-OK responses. */
|
||||
async function fetchErrorDetail(response: Response): Promise<string> {
|
||||
try {
|
||||
const body: unknown = await response.json();
|
||||
const detail = (body as { detail?: unknown } | null)?.detail;
|
||||
if (typeof detail === "string" && detail.trim()) {
|
||||
return ` — ${detail.trim()}`;
|
||||
}
|
||||
} catch {
|
||||
// Non-JSON or unreadable body: fall back to the status-only message.
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function fetchAllNodes(
|
||||
signal: AbortSignal,
|
||||
onProgress?: (progress: GraphLoadProgress) => void,
|
||||
@@ -335,7 +349,7 @@ async function fetchAllNodes(
|
||||
|
||||
const response = await fetch(url.toString(), { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
throw new Error(`Fetch failed: ${response.status}${await fetchErrorDetail(response)}`);
|
||||
}
|
||||
|
||||
const data: NodeListResponse = await response.json();
|
||||
@@ -390,7 +404,7 @@ async function fetchAllEdges(
|
||||
|
||||
const response = await fetch(url.toString(), { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
throw new Error(`Fetch failed: ${response.status}${await fetchErrorDetail(response)}`);
|
||||
}
|
||||
|
||||
const data: EdgeListResponse = await response.json();
|
||||
|
||||
+10
-2
@@ -49,7 +49,14 @@ dependencies = [
|
||||
"scipy>=1.13.1",
|
||||
"scikit-learn>=1.7.2",
|
||||
"umap-learn>=0.5.12",
|
||||
"spacy>=3.4.0",
|
||||
# thinc (spacy's core dep) dropped Python 3.9 wheels at 8.3.10, and later
|
||||
# spacy patch releases (3.8.8+) require thinc>=8.3.9-only-on-3.10+ ranges,
|
||||
# which forces a source build that fails outright on 3.9 (see Install
|
||||
# Matrix run history). Capping both keeps 3.9 on the last wheel-compatible
|
||||
# pair; 3.10+ is left unconstrained to always get the latest spacy/thinc.
|
||||
"spacy>=3.4.0,<3.8.8; python_version < '3.10'",
|
||||
"spacy>=3.4.0; python_version >= '3.10'",
|
||||
"thinc<8.3.5; python_version < '3.10'",
|
||||
"transformers>=4.20.0",
|
||||
"torch>=1.13.1",
|
||||
"sentence-transformers>=2.2.0",
|
||||
@@ -107,11 +114,12 @@ llm-gemini = ["google-genai>=0.1.0"]
|
||||
llm-anthropic = ["anthropic>=0.122.0"]
|
||||
llm-ollama = ["ollama>=0.1.0"]
|
||||
llm-deepseek = ["openai>=1.0.0"]
|
||||
llm-novita = ["openai>=1.0.0"]
|
||||
llm-litellm = ["litellm>=1.83.9"]
|
||||
llm-instructor = ["instructor>=1.15.3"]
|
||||
|
||||
llm-all = [
|
||||
"semantica[llm-openai,llm-groq,llm-gemini,llm-anthropic,llm-ollama,llm-deepseek,llm-litellm,llm-instructor]"
|
||||
"semantica[llm-openai,llm-groq,llm-gemini,llm-anthropic,llm-ollama,llm-deepseek,llm-novita,llm-litellm,llm-instructor]"
|
||||
]
|
||||
|
||||
# ---- Document Parsing ----
|
||||
|
||||
+114
-9
@@ -3714,19 +3714,61 @@ def store_stats(cli_ctx: CLIContext, backend: str, fmt: str, local_json: bool) -
|
||||
_run_with_error_handling(_action)
|
||||
|
||||
|
||||
_MIGRATE_SUPPORTED_BACKENDS = {"faiss", "sqlite", "pgvector"}
|
||||
_MIGRATE_BATCH_SIZE = 500
|
||||
|
||||
|
||||
def _migrate_backend_config(vs_cfg: Dict[str, Any], backend: str) -> Dict[str, Any]:
|
||||
"""Resolve per-backend config out of the vector_store config section.
|
||||
|
||||
Supports both a per-backend nested shape (``vector_store.faiss.dimension``)
|
||||
and the common flat single-backend shape (``vector_store.backend`` +
|
||||
sibling keys), since either can appear depending on how many backends a
|
||||
user has configured.
|
||||
"""
|
||||
nested = vs_cfg.get(backend)
|
||||
if isinstance(nested, dict):
|
||||
return dict(nested)
|
||||
if vs_cfg.get("backend") == backend:
|
||||
return {k: v for k, v in vs_cfg.items() if k != "backend"}
|
||||
return {}
|
||||
|
||||
|
||||
def _require_faiss_index_path(cfg: Dict[str, Any], role: str) -> str:
|
||||
"""FAISS has no server to hold state between commands: a fresh FAISSStore
|
||||
starts empty and nothing outside the process persists it, so migration
|
||||
needs an explicit on-disk index to read from or write to."""
|
||||
index_path = cfg.get("index_path")
|
||||
if not index_path:
|
||||
raise click.ClickException(
|
||||
f"faiss as migration {role} requires 'index_path' in the vector_store "
|
||||
f"config (vector_store.faiss.index_path or vector_store.index_path "
|
||||
f"when faiss is the configured backend)."
|
||||
)
|
||||
return index_path
|
||||
|
||||
|
||||
@store.command("migrate")
|
||||
@click.option("--from", "from_backend", required=True)
|
||||
@click.option("--to", "to_backend", required=True)
|
||||
@click.option("--namespace", default=None)
|
||||
@click.option("--dry-run", "local_dry", is_flag=True, default=False)
|
||||
@click.option("--json", "local_json", is_flag=True, default=False)
|
||||
@click.pass_obj
|
||||
def store_migrate(cli_ctx: CLIContext, from_backend: str, to_backend: str,
|
||||
namespace: Optional[str], local_dry: bool) -> None:
|
||||
namespace: Optional[str], local_dry: bool, local_json: bool) -> None:
|
||||
"""Migrate data between backends.
|
||||
|
||||
Direct migration is only wired up between faiss, sqlite, and pgvector -
|
||||
these are the backends whose storage contract supports paging through
|
||||
every stored vector. Migrating to or from qdrant, pinecone, milvus, or
|
||||
weaviate still needs the export/reindex workaround below, since each of
|
||||
those needs its own enumeration design (Qdrant scroll, Pinecone list,
|
||||
etc.) that hasn't been built yet.
|
||||
|
||||
\b
|
||||
Example:
|
||||
semantica store migrate --from faiss --to qdrant --namespace production --dry-run
|
||||
semantica store migrate --from faiss --to sqlite --namespace production --dry-run
|
||||
"""
|
||||
cli_ctx = _require_ctx(cli_ctx)
|
||||
|
||||
@@ -3734,13 +3776,76 @@ def store_migrate(cli_ctx: CLIContext, from_backend: str, to_backend: str,
|
||||
if _is_dry(cli_ctx, local_dry):
|
||||
_dry(cli_ctx, "migrate", from_backend=from_backend, to_backend=to_backend)
|
||||
return
|
||||
raise click.ClickException(
|
||||
f"Direct backend migration ({from_backend} → {to_backend}) is not yet supported "
|
||||
"by the vector store layer. To migrate, export your data first:\n"
|
||||
" semantica export --format parquet --output dump.parquet\n"
|
||||
f" semantica embed index dump.parquet --store {to_backend}"
|
||||
+ (f" --namespace {namespace}" if namespace else "")
|
||||
)
|
||||
|
||||
if from_backend not in _MIGRATE_SUPPORTED_BACKENDS or to_backend not in _MIGRATE_SUPPORTED_BACKENDS:
|
||||
raise click.ClickException(
|
||||
f"Direct backend migration ({from_backend} → {to_backend}) is only supported "
|
||||
f"between {', '.join(sorted(_MIGRATE_SUPPORTED_BACKENDS))}. To migrate involving "
|
||||
"another backend, export your data first:\n"
|
||||
" semantica export --format parquet --output dump.parquet\n"
|
||||
f" semantica embed index dump.parquet --store {to_backend}"
|
||||
+ (f" --namespace {namespace}" if namespace else "")
|
||||
)
|
||||
|
||||
from .vector_store import VectorStore
|
||||
|
||||
vs_cfg = cli_ctx.config.to_dict().get("vector_store", {}) or {}
|
||||
source_cfg = _migrate_backend_config(vs_cfg, from_backend)
|
||||
dest_cfg = _migrate_backend_config(vs_cfg, to_backend)
|
||||
|
||||
source_index_path = None
|
||||
if from_backend == "faiss":
|
||||
source_index_path = _require_faiss_index_path(source_cfg, "source")
|
||||
dest_index_path = None
|
||||
if to_backend == "faiss":
|
||||
dest_index_path = _require_faiss_index_path(dest_cfg, "destination")
|
||||
|
||||
source = VectorStore(backend=from_backend, config=source_cfg)
|
||||
if source_index_path:
|
||||
source._backend_store.load_index(source_index_path)
|
||||
|
||||
source_dimension = getattr(source._backend_store, "dimension", None)
|
||||
if source_dimension and "dimension" not in dest_cfg:
|
||||
dest_cfg["dimension"] = source_dimension
|
||||
|
||||
dest = VectorStore(backend=to_backend, config=dest_cfg)
|
||||
if dest_index_path and Path(dest_index_path).exists():
|
||||
dest._backend_store.load_index(dest_index_path)
|
||||
|
||||
migrated = 0
|
||||
vectors_batch: List[Any] = []
|
||||
metadata_batch: List[Dict[str, Any]] = []
|
||||
ids_batch: List[str] = []
|
||||
|
||||
def _flush() -> None:
|
||||
nonlocal migrated
|
||||
if not vectors_batch:
|
||||
return
|
||||
dest.store_vectors(list(vectors_batch), list(metadata_batch), ids=list(ids_batch))
|
||||
migrated += len(vectors_batch)
|
||||
vectors_batch.clear()
|
||||
metadata_batch.clear()
|
||||
ids_batch.clear()
|
||||
|
||||
for item in source.iter_vectors(batch_size=_MIGRATE_BATCH_SIZE):
|
||||
meta = dict(item.get("metadata") or {})
|
||||
if namespace and "namespace" not in meta:
|
||||
meta["namespace"] = namespace
|
||||
vectors_batch.append(item["vector"])
|
||||
metadata_batch.append(meta)
|
||||
ids_batch.append(item["id"])
|
||||
if len(vectors_batch) >= _MIGRATE_BATCH_SIZE:
|
||||
_flush()
|
||||
_flush()
|
||||
|
||||
if dest_index_path and migrated:
|
||||
dest._backend_store.save_index(dest_index_path)
|
||||
|
||||
result = {"from": from_backend, "to": to_backend, "migrated": migrated}
|
||||
if _is_json(cli_ctx, local_json):
|
||||
_jecho(result)
|
||||
else:
|
||||
_ok(cli_ctx, f"Migrated {migrated} vectors from {from_backend} to {to_backend}")
|
||||
|
||||
_run_with_error_handling(_action)
|
||||
|
||||
|
||||
@@ -1286,13 +1286,19 @@ class AgentMemory:
|
||||
"""
|
||||
return self.retrieve(content, max_results=limit, **kwargs)
|
||||
|
||||
def find_by_entity(self, entity_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
def find_by_entity(
|
||||
self, entity_id: str, limit: Optional[int] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Find by entity.
|
||||
|
||||
Args:
|
||||
entity_id: Entity ID to search for
|
||||
limit: Maximum results (default: 10)
|
||||
limit: Maximum results. None (the default) returns ALL matches.
|
||||
The previous default of 10 silently truncated results — an
|
||||
erasure workflow computing "what references this entity"
|
||||
from a truncated page would leave the remainder live
|
||||
(#1018). Callers that want pagination pass an explicit limit.
|
||||
|
||||
Returns:
|
||||
List of memory dicts containing the entity
|
||||
@@ -1308,9 +1314,9 @@ class AgentMemory:
|
||||
if mem_dict:
|
||||
results.append(mem_dict)
|
||||
break
|
||||
if len(results) >= limit:
|
||||
if limit is not None and len(results) >= limit:
|
||||
break
|
||||
return results[:limit]
|
||||
return results if limit is None else results[:limit]
|
||||
|
||||
def find_by_relationship(
|
||||
self, relationship_type: str, limit: int = 10
|
||||
|
||||
@@ -76,11 +76,11 @@ Production Use Cases:
|
||||
- Insurance: Claim decisions, underwriting assessments
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
import json
|
||||
import uuid
|
||||
from dataclasses import InitVar, dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -100,8 +100,9 @@ class Decision:
|
||||
valid_from: Optional[str] = None
|
||||
valid_until: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
"""Validate decision data."""
|
||||
if auto_generate_id and not self.decision_id: # Handle both None and empty string
|
||||
self.decision_id = str(uuid.uuid4())
|
||||
@@ -146,8 +147,9 @@ class DecisionContext:
|
||||
risk_factors: List[str]
|
||||
cross_system_inputs: Dict[str, Any] = field(default_factory=dict)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
"""Validate decision context data."""
|
||||
if auto_generate_id and not self.context_id: # Handle both None and empty string
|
||||
self.context_id = str(uuid.uuid4())
|
||||
@@ -184,8 +186,9 @@ class Policy:
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
"""Validate policy data."""
|
||||
if auto_generate_id and not self.policy_id: # Handle both None and empty string
|
||||
self.policy_id = str(uuid.uuid4())
|
||||
@@ -227,8 +230,9 @@ class PolicyException:
|
||||
approval_timestamp: datetime
|
||||
justification: str
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
"""Validate policy exception data."""
|
||||
if auto_generate_id and not self.exception_id: # Handle both None and empty string
|
||||
self.exception_id = str(uuid.uuid4())
|
||||
@@ -265,8 +269,9 @@ class Precedent:
|
||||
similarity_score: float
|
||||
relationship_type: str # "similar_scenario", "same_policy", "exception_precedent"
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
"""Validate precedent data."""
|
||||
if auto_generate_id and not self.precedent_id: # Handle both None and empty string
|
||||
self.precedent_id = str(uuid.uuid4())
|
||||
@@ -305,8 +310,9 @@ class ApprovalChain:
|
||||
approval_context: str
|
||||
timestamp: datetime
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
"""Validate approval chain data."""
|
||||
if auto_generate_id and not self.approval_id: # Handle both None and empty string
|
||||
self.approval_id = str(uuid.uuid4())
|
||||
|
||||
@@ -10,28 +10,53 @@ Supported Providers:
|
||||
- OpenAI: OpenAI API (GPT-3.5, GPT-4, etc.)
|
||||
- HuggingFaceLLM: HuggingFace Transformers for local LLM inference
|
||||
- LiteLLM: Unified interface to 100+ LLM providers (OpenAI, Anthropic, Groq, Azure, Bedrock, Vertex AI, etc.)
|
||||
- Anthropic: Anthropic Claude API (Claude sonnet, Opus, Haiku, etc.)
|
||||
- Gemini: Google Gemini API
|
||||
- Ollama: Local models served through Ollama
|
||||
- DeepSeek: DeepSeek's OpenAI-compatible API
|
||||
- Novita: Novita AI's OpenAI-compatible API
|
||||
|
||||
Example Usage:
|
||||
>>> from semantica.llms import Groq, OpenAI, HuggingFaceLLM, LiteLLM
|
||||
>>>
|
||||
>>> from semantica.llms import Groq, OpenAI, HuggingFaceLLM, LiteLLM, Anthropic
|
||||
>>>
|
||||
>>> # Groq provider
|
||||
>>> groq = Groq(model="llama-3.1-8b-instant", api_key="your-key")
|
||||
>>> response = groq.generate("Hello, world!")
|
||||
>>>
|
||||
>>>
|
||||
>>> # OpenAI provider
|
||||
>>> openai = OpenAI(model="gpt-4", api_key="your-key")
|
||||
>>> response = openai.generate("Hello, world!")
|
||||
>>>
|
||||
>>>
|
||||
>>> # HuggingFace LLM provider
|
||||
>>> hf = HuggingFaceLLM(model_name="gpt2")
|
||||
>>> response = hf.generate("Hello, world!")
|
||||
>>>
|
||||
>>>
|
||||
>>> # LiteLLM provider (supports 100+ LLMs)
|
||||
>>> llm = LiteLLM(model="openai/gpt-4o", api_key="your-key")
|
||||
>>> response = llm.generate("Hello, world!")
|
||||
>>> # Or use other providers via LiteLLM
|
||||
>>> llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
|
||||
>>> response = llm.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Anthropic provider
|
||||
>>> claude = Anthropic(model="claude-sonnet-4-6", api_key="the-key")
|
||||
>>> response = claude.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Gemini provider
|
||||
>>> gemini = Gemini(model="gemini-pro", api_key="your-key")
|
||||
>>> response = gemini.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Ollama provider (local, no api_key)
|
||||
>>> ollama = Ollama(model="llama2")
|
||||
>>> response = ollama.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # DeepSeek provider
|
||||
>>> deepseek = DeepSeek(model="deepseek-chat", api_key="your-key")
|
||||
>>> response = deepseek.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Novita provider
|
||||
>>> novita = Novita(model="deepseek/deepseek-v3.2", api_key="your-key")
|
||||
>>> response = novita.generate("Hello, world!")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
@@ -41,6 +66,20 @@ from .groq import Groq
|
||||
from .openai import OpenAI
|
||||
from .huggingface import HuggingFaceLLM
|
||||
from .litellm import LiteLLM
|
||||
from .anthropic import Anthropic
|
||||
from .gemini import Gemini
|
||||
from .ollama import Ollama
|
||||
from .deepseek import DeepSeek
|
||||
from .novita import Novita
|
||||
|
||||
__all__ = ["Groq", "OpenAI", "HuggingFaceLLM", "LiteLLM"]
|
||||
|
||||
__all__ = [
|
||||
"Groq",
|
||||
"OpenAI",
|
||||
"HuggingFaceLLM",
|
||||
"LiteLLM",
|
||||
"Anthropic",
|
||||
"Gemini",
|
||||
"Ollama",
|
||||
"DeepSeek",
|
||||
"Novita",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Anthropic LLM Provider
|
||||
|
||||
Wrapper for Anthropic Claude API provider with clean interface
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import AnthropicProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.anthropic")
|
||||
|
||||
|
||||
class Anthropic:
|
||||
"""
|
||||
Anthropic Claude LLM provider wrapper.
|
||||
|
||||
Provides clean interface to Anthropic's Claude API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Anthropic
|
||||
>>> claude = Anthropic(model="claude-sonnet-4-6", api_key="the-key")
|
||||
>>> response = claude.generate("What is API key?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "claude-sonnet-4-6",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Anthropic provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: claude-sonnet-4-6)
|
||||
api_key: Anthropic API key (default: from ANTHROPIC_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = AnthropicProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Anthropic provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generates structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
DeepSeek LLM Provider
|
||||
|
||||
Wrapper for DeepSeek API provider with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import DeepSeekProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.deepseek")
|
||||
|
||||
|
||||
class DeepSeek:
|
||||
"""
|
||||
DeepSeek LLM provider wrapper.
|
||||
|
||||
Provides clean interface to DeepSeek's OpenAI-compatible API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import DeepSeek
|
||||
>>> llm = DeepSeek(model="deepseek-chat", api_key="your-key")
|
||||
>>> response = llm.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "deepseek-chat",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize DeepSeek provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "deepseek-chat")
|
||||
api_key: DeepSeek API key (default: from DEEPSEEK_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = DeepSeekProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if DeepSeek provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"DeepSeek provider not available. Set DEEPSEEK_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"DeepSeek provider not available. Set DEEPSEEK_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"DeepSeek provider not available. Set DEEPSEEK_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Gemini LLM Provider
|
||||
|
||||
Wrapper for Google Gemini API provider with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import GeminiProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.gemini")
|
||||
|
||||
|
||||
class Gemini:
|
||||
"""
|
||||
Google Gemini LLM provider wrapper.
|
||||
|
||||
Provides clean interface to Google's Gemini API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Gemini
|
||||
>>> gemini = Gemini(model="gemini-pro", api_key="your-key")
|
||||
>>> response = gemini.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "gemini-pro",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Gemini provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "gemini-pro")
|
||||
api_key: Gemini API key (default: from GEMINI_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = GeminiProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Gemini provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Gemini provider not available. Set GEMINI_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or parsing fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Gemini provider not available. Set GEMINI_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Gemini provider not available. Set GEMINI_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Novita LLM Provider
|
||||
|
||||
Wrapper for Novita AI's OpenAI-compatible API with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import NovitaProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.novita")
|
||||
|
||||
|
||||
class Novita:
|
||||
"""
|
||||
Novita AI LLM provider wrapper.
|
||||
|
||||
Provides clean interface to Novita's OpenAI-compatible API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Novita
|
||||
>>> llm = Novita(model="deepseek/deepseek-v3.2", api_key="your-key")
|
||||
>>> response = llm.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "deepseek/deepseek-v3.2",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Novita provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "deepseek/deepseek-v3.2")
|
||||
api_key: Novita API key (default: from NOVITA_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = NovitaProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Novita provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Novita provider not available. Set NOVITA_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Novita provider not available. Set NOVITA_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Novita provider not available. Set NOVITA_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Ollama LLM Provider
|
||||
|
||||
Wrapper for local Ollama models with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from ..semantic_extract.providers import OllamaProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.ollama")
|
||||
|
||||
|
||||
class Ollama:
|
||||
"""
|
||||
Ollama LLM provider wrapper.
|
||||
|
||||
Provides clean interface to a local Ollama server. Unlike the other
|
||||
providers here, this one has no API key. It talks to an Ollama
|
||||
instance over HTTP, so make sure `ollama serve` is running first.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Ollama
|
||||
>>> llm = Ollama(model="llama2")
|
||||
>>> response = llm.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "llama2",
|
||||
base_url: str = "http://localhost:11434",
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Ollama provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "llama2")
|
||||
base_url: Ollama server URL (default: "http://localhost:11434")
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = OllamaProvider(base_url=base_url, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.base_url = base_url
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Ollama provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Ollama provider not available. Make sure Ollama is running "
|
||||
"and reachable at the configured base_url."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or parsing fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Ollama provider not available. Make sure Ollama is running "
|
||||
"and reachable at the configured base_url."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Ollama provider not available. Make sure Ollama is running "
|
||||
"and reachable at the configured base_url."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -41,69 +41,134 @@ from ..utils.logging import get_logger
|
||||
from ..utils.progress_tracker import get_progress_tracker
|
||||
from .reasoner import Fact, Rule, _make_activation_key
|
||||
|
||||
logger = get_logger("rete_engine")
|
||||
|
||||
def _extract_bindings(condition: Any, fact: Fact) -> Dict[str, Any]:
|
||||
"""Extract ``?var`` bindings by matching a condition pattern against a fact.
|
||||
|
||||
``condition`` is the pattern stored on the alpha node (typically a string
|
||||
like ``"Person(?x)"``); ``fact`` is the working-memory :class:`Fact`. The
|
||||
fact's canonical string form (``Predicate(arg1, arg2, ...)``) is matched
|
||||
against the pattern using the same ``?\\w+`` placeholder convention as the
|
||||
Reasoner, so downstream actions receive real bindings (e.g. ``{"x": "John"}``)
|
||||
instead of the empty dict that previously left ``?x`` placeholders
|
||||
unsubstituted.
|
||||
def _build_condition_regex(
|
||||
pattern: str,
|
||||
initial_bindings: Optional[Dict[str, str]] = None,
|
||||
) -> str:
|
||||
"""Build an anchored regex string for a condition pattern.
|
||||
|
||||
Returns an empty dict when the condition is not a string pattern or does
|
||||
not match -- callers treat that as "no bindings extracted".
|
||||
Splits the pattern on ``?var`` placeholders, escaping the literal
|
||||
segments so surrounding parentheses/commas match literally. Variables
|
||||
become named groups (or backreferences when repeated); variables already
|
||||
present in ``initial_bindings`` are inlined as their literal value.
|
||||
|
||||
Args:
|
||||
pattern: The condition pattern string (e.g. ``"Person(?x)"``).
|
||||
initial_bindings: Bindings already established upstream. Variables
|
||||
already bound are matched as literals rather than captured.
|
||||
|
||||
Returns:
|
||||
An anchored regex string (``^...$``) suitable for ``re.compile`` /
|
||||
``re.match``.
|
||||
"""
|
||||
if not isinstance(condition, str):
|
||||
return {}
|
||||
|
||||
segments = re.split(r"(\?\w+)", condition)
|
||||
bindings = initial_bindings or {}
|
||||
segments = re.split(r"(\?\w+)", pattern)
|
||||
seen_vars: Set[str] = set()
|
||||
p_regex = ""
|
||||
for seg in segments:
|
||||
if seg.startswith("?"):
|
||||
var_name = seg[1:]
|
||||
if var_name in seen_vars:
|
||||
if var_name in bindings:
|
||||
# Already bound — require the exact literal value.
|
||||
p_regex += re.escape(bindings[var_name])
|
||||
elif var_name in seen_vars:
|
||||
# Same variable used twice — enforce a backreference.
|
||||
p_regex += f"(?P={var_name})"
|
||||
else:
|
||||
p_regex += f"(?P<{var_name}>.+?)"
|
||||
seen_vars.add(var_name)
|
||||
else:
|
||||
p_regex += re.escape(seg)
|
||||
p_regex = f"^{p_regex}$"
|
||||
return f"^{p_regex}$"
|
||||
|
||||
|
||||
def unify_condition(
|
||||
condition: Any,
|
||||
fact: Fact,
|
||||
initial_bindings: Optional[Dict[str, str]] = None,
|
||||
) -> Optional[Dict[str, str]]:
|
||||
"""Unify a condition pattern against a fact.
|
||||
|
||||
A condition is a pattern string such as ``"Person(?x)"`` or
|
||||
``"knows(?x, ?y)"`` where tokens beginning with ``?`` are variables.
|
||||
The fact is rendered via its ``__str__`` representation
|
||||
(``predicate(arg1, arg2)``) and matched against the pattern.
|
||||
|
||||
This mirrors ``Reasoner._match_pattern`` but is self-contained so the
|
||||
RETE engine does not need a live ``Reasoner`` instance.
|
||||
|
||||
Args:
|
||||
condition: The condition pattern (string). Non-string conditions
|
||||
are stringified before matching.
|
||||
fact: The fact to test.
|
||||
initial_bindings: Bindings already established upstream. Variables
|
||||
already bound must match the corresponding literal in the fact.
|
||||
|
||||
Returns:
|
||||
A dict of variable bindings if the fact unifies with the condition,
|
||||
otherwise ``None``.
|
||||
"""
|
||||
bindings = dict(initial_bindings or {})
|
||||
pattern = condition if isinstance(condition, str) else str(condition)
|
||||
fact_str = str(fact)
|
||||
|
||||
# Build the anchored regex once (variables already bound are inlined as
|
||||
# literals). See ``_build_condition_regex`` for the segment handling.
|
||||
p_regex = _build_condition_regex(pattern, bindings)
|
||||
|
||||
try:
|
||||
match = re.match(p_regex, str(fact))
|
||||
except re.error:
|
||||
return {}
|
||||
match = re.match(p_regex, fact_str)
|
||||
except re.error as e:
|
||||
logger.warning(
|
||||
"unify_condition failed to compile/match condition "
|
||||
"%r (regex: %r) against fact %r: %s",
|
||||
pattern,
|
||||
p_regex,
|
||||
fact_str,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
except Exception as e: # noqa: BLE001 - mirror Reasoner._match_pattern
|
||||
logger.warning(
|
||||
"unify_condition unexpected error matching condition "
|
||||
"%r (regex: %r) against fact %r: %s",
|
||||
pattern,
|
||||
p_regex,
|
||||
fact_str,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
if not match:
|
||||
return {}
|
||||
return {k: v for k, v in match.groupdict().items() if v is not None}
|
||||
return None
|
||||
|
||||
|
||||
def _bindings_for_rule(rule: Rule, facts: List[Fact]) -> Dict[str, Any]:
|
||||
"""Merge ``?var`` bindings from matching a rule's conditions against facts.
|
||||
|
||||
Each fact is matched against every condition of the rule; the first
|
||||
condition that yields bindings for a fact contributes them. Bindings from
|
||||
all facts are merged so multi-condition (joined) rules receive the full
|
||||
variable environment. Later conflicting values do not overwrite earlier
|
||||
ones, preserving the binding that a join already validated.
|
||||
"""
|
||||
bindings: Dict[str, Any] = {}
|
||||
for fact in facts:
|
||||
for condition in rule.conditions:
|
||||
extracted = _extract_bindings(condition, fact)
|
||||
if not extracted:
|
||||
continue
|
||||
for key, value in extracted.items():
|
||||
bindings.setdefault(key, value)
|
||||
break
|
||||
for var, value in match.groupdict().items():
|
||||
if var in bindings and bindings[var] != value:
|
||||
return None # Binding conflict.
|
||||
bindings[var] = value
|
||||
return bindings
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
"""A partial match flowing through the Rete network.
|
||||
|
||||
A token represents an ordered collection of concrete facts that have
|
||||
been unified so far, together with the consistent variable bindings
|
||||
accumulated across those facts.
|
||||
|
||||
Alpha nodes emit single-fact tokens. Beta nodes merge a left token and
|
||||
a right token into a new token whose ``facts`` are the concatenation of
|
||||
both sides (preserving condition order) and whose ``bindings`` are the
|
||||
consistent union of both sides.
|
||||
"""
|
||||
|
||||
facts: List[Fact] = field(default_factory=list)
|
||||
bindings: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Match:
|
||||
"""Pattern match."""
|
||||
@@ -128,19 +193,68 @@ class AlphaNode(ReteNode):
|
||||
def __init__(self, node_id: str, condition: Any):
|
||||
super().__init__(node_id)
|
||||
self.condition = condition
|
||||
self.matches: List[Fact] = []
|
||||
# Single-fact tokens produced by unifying each matched fact with
|
||||
# this node's condition.
|
||||
self.tokens: List[Token] = []
|
||||
# Pre-compile the condition regex once. Alpha nodes never have
|
||||
# initial bindings, so the pattern is stable for the node's lifetime
|
||||
# and every incoming fact reuses this compiled matcher instead of
|
||||
# rebuilding it (avoids repeated regex construction overhead).
|
||||
pattern = condition if isinstance(condition, str) else str(condition)
|
||||
self._compiled: Optional[re.Pattern] = None
|
||||
try:
|
||||
self._compiled = re.compile(_build_condition_regex(pattern))
|
||||
except re.error as e:
|
||||
logger.warning(
|
||||
"AlphaNode %r failed to compile condition %r: %s; "
|
||||
"node will never match",
|
||||
node_id,
|
||||
pattern,
|
||||
e,
|
||||
)
|
||||
|
||||
def add_fact(self, fact: Fact) -> bool:
|
||||
"""Add fact if it matches condition."""
|
||||
if self._matches(fact):
|
||||
self.matches.append(fact)
|
||||
return True
|
||||
return False
|
||||
def add_fact(self, fact: Fact) -> Optional[Token]:
|
||||
"""Add fact if it matches the condition, returning its token.
|
||||
|
||||
def _matches(self, fact: Fact) -> bool:
|
||||
"""Check if fact matches condition."""
|
||||
# Simple matching - can be enhanced
|
||||
return True
|
||||
Returns the single-fact ``Token`` produced by unification when the
|
||||
fact matches, otherwise ``None``.
|
||||
"""
|
||||
bindings = self._matches(fact)
|
||||
if bindings is not None:
|
||||
token = Token(facts=[fact], bindings=dict(bindings))
|
||||
self.tokens.append(token)
|
||||
return token
|
||||
return None
|
||||
|
||||
def _matches(self, fact: Fact) -> Optional[Dict[str, str]]:
|
||||
"""Check if fact matches the alpha node condition.
|
||||
|
||||
Uses the pre-compiled regex built in ``__init__`` for performance,
|
||||
since RETE evaluates many facts against every alpha node.
|
||||
|
||||
Returns the variable bindings produced by unification if the fact
|
||||
matches, otherwise ``None``. An empty dict signals a match with no
|
||||
variables (still distinct from ``None``).
|
||||
"""
|
||||
if self._compiled is None:
|
||||
# Compilation failed at build time; treat as non-matching.
|
||||
return None
|
||||
fact_str = str(fact)
|
||||
try:
|
||||
match = self._compiled.match(fact_str)
|
||||
except Exception as e: # noqa: BLE001 - mirror unify_condition
|
||||
logger.warning(
|
||||
"AlphaNode %r unexpected error matching condition "
|
||||
"%r against fact %r: %s",
|
||||
self.node_id,
|
||||
self.condition,
|
||||
fact_str,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
if not match:
|
||||
return None
|
||||
return match.groupdict()
|
||||
|
||||
|
||||
class BetaNode(ReteNode):
|
||||
@@ -150,19 +264,28 @@ class BetaNode(ReteNode):
|
||||
super().__init__(node_id)
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.matches: List[Tuple[Fact, Fact]] = []
|
||||
# Token memories for each side. Incoming tokens are stored here so
|
||||
# that later-arriving tokens on the opposite side can be joined
|
||||
# against every token already seen (chained joins).
|
||||
self.left_tokens: List[Token] = []
|
||||
self.right_tokens: List[Token] = []
|
||||
|
||||
def join(self, left_fact: Fact, right_fact: Fact) -> bool:
|
||||
"""Join facts from left and right nodes."""
|
||||
if self._can_join(left_fact, right_fact):
|
||||
self.matches.append((left_fact, right_fact))
|
||||
return True
|
||||
return False
|
||||
def join(self, left_token: Token, right_token: Token) -> Optional[Token]:
|
||||
"""Join a left token with a right token.
|
||||
|
||||
def _can_join(self, left_fact: Fact, right_fact: Fact) -> bool:
|
||||
"""Check if facts can be joined."""
|
||||
# Simple join logic - can be enhanced
|
||||
return True
|
||||
Returns a new merged ``Token`` (facts concatenated in condition
|
||||
order, bindings unified) when the two tokens are consistent,
|
||||
otherwise ``None`` on a binding conflict.
|
||||
"""
|
||||
merged = dict(left_token.bindings)
|
||||
for var, value in right_token.bindings.items():
|
||||
if var in merged and merged[var] != value:
|
||||
return None # Binding conflict — cannot join.
|
||||
merged[var] = value
|
||||
return Token(
|
||||
facts=list(left_token.facts) + list(right_token.facts),
|
||||
bindings=merged,
|
||||
)
|
||||
|
||||
|
||||
class TerminalNode(ReteNode):
|
||||
@@ -248,12 +371,16 @@ class ReteEngine:
|
||||
self._add_rule_to_network(rule)
|
||||
|
||||
self.logger.info(
|
||||
f"Built Rete network with {len(self.network)} nodes for {len(rules)} rules"
|
||||
f"Built Rete network with {len(self.network)} nodes "
|
||||
f"for {len(rules)} rules"
|
||||
)
|
||||
self.progress_tracker.stop_tracking(
|
||||
tracking_id,
|
||||
status="completed",
|
||||
message=f"Built Rete network with {len(self.network)} nodes for {len(rules)} rules",
|
||||
message=(
|
||||
f"Built Rete network with {len(self.network)} nodes "
|
||||
f"for {len(rules)} rules"
|
||||
),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -281,6 +408,10 @@ class ReteEngine:
|
||||
self.node_counter += 1
|
||||
beta_node = BetaNode(node_id, current, alpha_nodes[i])
|
||||
self.network[node_id] = beta_node
|
||||
# Wire the beta node as a child of both its inputs so facts
|
||||
# propagating from either side reach the join.
|
||||
current.children.append(beta_node)
|
||||
alpha_nodes[i].children.append(beta_node)
|
||||
current = beta_node
|
||||
final_node = current
|
||||
else:
|
||||
@@ -311,40 +442,58 @@ class ReteEngine:
|
||||
# Find matching alpha nodes
|
||||
for node_id, node in self.network.items():
|
||||
if isinstance(node, AlphaNode):
|
||||
if node.add_fact(fact):
|
||||
# Propagate to children
|
||||
self._propagate_from_alpha(node, fact)
|
||||
token = node.add_fact(fact)
|
||||
if token is not None:
|
||||
# Propagate the single-fact token to children.
|
||||
self._propagate_token(node, token)
|
||||
|
||||
def _propagate_from_alpha(self, alpha_node: AlphaNode, fact: Fact) -> None:
|
||||
"""Propagate from alpha node to children."""
|
||||
for child in alpha_node.children:
|
||||
def _propagate_token(self, source: ReteNode, token: Token) -> None:
|
||||
"""Propagate ``token`` (arriving from ``source``) to its children.
|
||||
|
||||
A ``Token`` carries the ordered facts and consistent bindings of a
|
||||
partial match. Beta children attempt joins and, on success, emit a
|
||||
new merged token downstream; terminal children turn the token into a
|
||||
rule activation using the token's complete facts and bindings.
|
||||
"""
|
||||
for child in source.children:
|
||||
if isinstance(child, BetaNode):
|
||||
# Join with matches from left side
|
||||
for left_fact in alpha_node.matches:
|
||||
if child.join(left_fact, fact):
|
||||
# Propagate to children
|
||||
for grandchild in child.children:
|
||||
if isinstance(grandchild, TerminalNode):
|
||||
facts = [left_fact, fact]
|
||||
match = Match(
|
||||
rule=grandchild.rule,
|
||||
facts=facts,
|
||||
bindings=_bindings_for_rule(
|
||||
grandchild.rule, facts
|
||||
),
|
||||
confidence=1.0,
|
||||
)
|
||||
grandchild.activate(match)
|
||||
self._propagate_to_beta(child, source, token)
|
||||
elif isinstance(child, TerminalNode):
|
||||
# Direct activation
|
||||
match = Match(
|
||||
rule=child.rule,
|
||||
facts=[fact],
|
||||
bindings=_bindings_for_rule(child.rule, [fact]),
|
||||
facts=list(token.facts),
|
||||
bindings=dict(token.bindings),
|
||||
confidence=1.0,
|
||||
)
|
||||
child.activate(match)
|
||||
|
||||
def _propagate_to_beta(
|
||||
self,
|
||||
beta: "BetaNode",
|
||||
source: ReteNode,
|
||||
token: Token,
|
||||
) -> None:
|
||||
"""Attempt joins at ``beta`` for a token arriving from one side.
|
||||
|
||||
The incoming token is stored in the corresponding side's memory,
|
||||
then joined against every token already recorded on the opposite
|
||||
side. Each successful join produces a new merged token that is
|
||||
propagated further downstream, enabling correct chained joins across
|
||||
three or more conditions.
|
||||
"""
|
||||
if source is beta.left:
|
||||
beta.left_tokens.append(token)
|
||||
for right_token in list(beta.right_tokens):
|
||||
merged = beta.join(token, right_token)
|
||||
if merged is not None:
|
||||
self._propagate_token(beta, merged)
|
||||
elif source is beta.right:
|
||||
beta.right_tokens.append(token)
|
||||
for left_token in list(beta.left_tokens):
|
||||
merged = beta.join(left_token, token)
|
||||
if merged is not None:
|
||||
self._propagate_token(beta, merged)
|
||||
|
||||
def match_patterns(self, facts: Optional[List[Fact]] = None) -> List[Match]:
|
||||
"""
|
||||
Match patterns using Rete algorithm.
|
||||
@@ -468,8 +617,11 @@ class ReteEngine:
|
||||
self.facts.clear()
|
||||
self.reset_action_history()
|
||||
for node in self.network.values():
|
||||
if isinstance(node, AlphaNode) or isinstance(node, BetaNode):
|
||||
node.matches.clear()
|
||||
if isinstance(node, AlphaNode):
|
||||
node.tokens.clear()
|
||||
elif isinstance(node, BetaNode):
|
||||
node.left_tokens.clear()
|
||||
node.right_tokens.clear()
|
||||
elif isinstance(node, TerminalNode):
|
||||
node.activations.clear()
|
||||
|
||||
|
||||
@@ -673,6 +673,7 @@ class GeminiProvider(BaseProvider):
|
||||
self.model = model
|
||||
self.client = None
|
||||
self._use_new_genai = False
|
||||
self._legacy_model_cache: Dict[str, Any] = {}
|
||||
self._init_client()
|
||||
|
||||
def _init_client(self):
|
||||
@@ -694,6 +695,38 @@ class GeminiProvider(BaseProvider):
|
||||
self.client = None
|
||||
self.logger.warning("Gemini SDK not installed. Install with: pip install semantica[llm-gemini]")
|
||||
|
||||
def _legacy_client_for(self, requested_model: str):
|
||||
"""Return a legacy-SDK GenerativeModel bound to this instance's own
|
||||
API key, for the given model name.
|
||||
|
||||
The legacy google-generativeai package keeps its API key as
|
||||
module-level state (genai.configure()), so any GenerativeModel built
|
||||
by a different GeminiProvider instance in the same process can leave
|
||||
that state pointing at a different key. Re-asserting configure()
|
||||
with this instance's key right before use, instead of only once at
|
||||
construction, keeps sequential calls across instances from reading
|
||||
each other's credentials. A cache keyed by model name avoids
|
||||
rebuilding a GenerativeModel on every call for the common case of
|
||||
one model being reused.
|
||||
"""
|
||||
try:
|
||||
import google.generativeai as old_genai
|
||||
old_genai.configure(api_key=self.api_key)
|
||||
except Exception:
|
||||
# _init_client() already required this import to reach the
|
||||
# legacy path in the first place, so this only happens when
|
||||
# self.client was injected directly (tests). Fall back to it
|
||||
# without reasserting credentials rather than failing calls
|
||||
# that never needed the real SDK.
|
||||
return self.client
|
||||
if requested_model == self.model:
|
||||
return self.client
|
||||
cached = self._legacy_model_cache.get(requested_model)
|
||||
if cached is None:
|
||||
cached = old_genai.GenerativeModel(requested_model)
|
||||
self._legacy_model_cache[requested_model] = cached
|
||||
return cached
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if provider is available."""
|
||||
return self.client is not None
|
||||
@@ -724,7 +757,8 @@ class GeminiProvider(BaseProvider):
|
||||
)
|
||||
return self._resp_text(resp)
|
||||
else:
|
||||
response = self.client.generate_content(prompt, generation_config=config or None)
|
||||
legacy_client = self._legacy_client_for(kwargs.get("model", self.model))
|
||||
response = legacy_client.generate_content(prompt, generation_config=config or None)
|
||||
return self._resp_text(response)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> dict:
|
||||
@@ -733,15 +767,24 @@ class GeminiProvider(BaseProvider):
|
||||
raise ProcessingError("Gemini client not initialized.")
|
||||
|
||||
json_prompt = f"{prompt}\n\nReturn the response as valid JSON only."
|
||||
|
||||
config = {}
|
||||
self._add_if_set(config, kwargs, "temperature", "top_p", "top_k", "stop_sequences", "candidate_count")
|
||||
if "max_tokens" in kwargs:
|
||||
config["max_output_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
if self._use_new_genai:
|
||||
model = kwargs.get("model", self.model)
|
||||
resp = self.client.models.generate_content(model=model, contents=json_prompt)
|
||||
resp = self.client.models.generate_content(
|
||||
model=model, contents=json_prompt, config=config or None
|
||||
)
|
||||
try:
|
||||
return self._parse_json(self._resp_text(resp))
|
||||
except Exception as e:
|
||||
raise ProcessingError(f"Failed to parse JSON from Gemini response: {e}")
|
||||
else:
|
||||
response = self.client.generate_content(json_prompt)
|
||||
legacy_client = self._legacy_client_for(kwargs.get("model", self.model))
|
||||
response = legacy_client.generate_content(json_prompt, generation_config=config or None)
|
||||
try:
|
||||
return self._parse_json(self._resp_text(response))
|
||||
except Exception as e:
|
||||
@@ -967,6 +1010,8 @@ class OllamaProvider(BaseProvider):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if provider is available."""
|
||||
if self.client is None:
|
||||
self._init_client()
|
||||
return self.client is not None
|
||||
|
||||
def _build_options(self, kwargs: dict) -> Optional[dict]:
|
||||
@@ -1018,7 +1063,6 @@ class DeepSeekProvider(BaseProvider):
|
||||
self.api_key = api_key or config.get_api_key("deepseek")
|
||||
self.base_url = "https://api.deepseek.com/v1"
|
||||
self.model = model
|
||||
self.base_url = "https://api.deepseek.com/v1"
|
||||
self.client = None
|
||||
self._init_client()
|
||||
|
||||
@@ -1045,7 +1089,7 @@ class DeepSeekProvider(BaseProvider):
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
return response.choices[0].message.content
|
||||
@@ -1058,8 +1102,9 @@ class DeepSeekProvider(BaseProvider):
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
try:
|
||||
@@ -1103,7 +1148,7 @@ class NovitaProvider(BaseProvider):
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
return response.choices[0].message.content
|
||||
@@ -1118,7 +1163,7 @@ class NovitaProvider(BaseProvider):
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
try:
|
||||
|
||||
@@ -64,6 +64,8 @@ class SlidingWindowChunker:
|
||||
raise ValidationError("overlap must be non-negative")
|
||||
if self.overlap >= self.chunk_size:
|
||||
raise ValidationError("overlap must be less than chunk_size")
|
||||
if self.stride <= 0:
|
||||
raise ValidationError("stride must be positive")
|
||||
|
||||
def chunk(self, text: str, **options) -> List[Chunk]:
|
||||
"""
|
||||
@@ -215,15 +217,20 @@ class SlidingWindowChunker:
|
||||
Returns:
|
||||
list: List of chunks
|
||||
"""
|
||||
if overlap_size is None:
|
||||
return self.chunk(text)
|
||||
if overlap_size < 0:
|
||||
raise ValidationError("overlap_size must be non-negative")
|
||||
if overlap_size >= self.chunk_size:
|
||||
raise ValidationError("overlap_size must be less than chunk_size")
|
||||
|
||||
original_overlap = self.overlap
|
||||
if overlap_size is not None:
|
||||
original_stride = self.stride
|
||||
|
||||
try:
|
||||
self.overlap = overlap_size
|
||||
self.stride = self.chunk_size - self.overlap
|
||||
|
||||
chunks = self.chunk(text)
|
||||
|
||||
# Restore original overlap
|
||||
self.overlap = original_overlap
|
||||
self.stride = self.chunk_size - self.overlap
|
||||
|
||||
return chunks
|
||||
return self.chunk(text)
|
||||
finally:
|
||||
self.overlap = original_overlap
|
||||
self.stride = original_stride
|
||||
|
||||
@@ -43,7 +43,7 @@ from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple, Union
|
||||
|
||||
from .logging import get_logger
|
||||
|
||||
@@ -144,14 +144,37 @@ class ProgressDisplay(ABC):
|
||||
class ConsoleProgressDisplay(ProgressDisplay):
|
||||
"""Console progress display with real-time updates."""
|
||||
|
||||
def __init__(self, use_emoji: bool = True, update_interval: float = 0.1):
|
||||
def __init__(
|
||||
self,
|
||||
use_emoji: bool = True,
|
||||
update_interval: float = 0.1,
|
||||
stream: Optional[TextIO] = None,
|
||||
):
|
||||
"""Initialize the console display.
|
||||
|
||||
Args:
|
||||
use_emoji: Whether to decorate output with emoji.
|
||||
update_interval: Minimum seconds between redraws.
|
||||
stream: Where progress is written. Defaults to ``sys.stderr``.
|
||||
|
||||
Progress is diagnostic output, so stderr is the correct stream
|
||||
for it, and stdout must stay clean for programs that carry a
|
||||
machine-readable protocol on it — the stdio MCP servers put
|
||||
newline-delimited JSON-RPC there, and a progress bar on stdout
|
||||
corrupts that stream.
|
||||
|
||||
Left as ``None``, the stream is resolved on each write rather
|
||||
than captured here, so a later rebinding of ``sys.stderr``
|
||||
(pytest capture, for instance) is honoured.
|
||||
"""
|
||||
self.use_emoji = use_emoji
|
||||
|
||||
# Check if stdout supports emojis (especially on Windows)
|
||||
self._stream = stream
|
||||
|
||||
# Check if the target stream supports emojis (especially on Windows)
|
||||
if self.use_emoji:
|
||||
try:
|
||||
# Try encoding a test emoji with stdout's encoding
|
||||
encoding = getattr(sys.stdout, "encoding", None)
|
||||
# Try encoding a test emoji with the stream's encoding
|
||||
encoding = getattr(self.stream, "encoding", None)
|
||||
if encoding:
|
||||
"🧠".encode(encoding)
|
||||
except (UnicodeEncodeError, LookupError, AttributeError):
|
||||
@@ -162,6 +185,11 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
self.current_lines: Dict[str, str] = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
@property
|
||||
def stream(self) -> TextIO:
|
||||
"""The stream progress is written to; ``sys.stderr`` unless overridden."""
|
||||
return self._stream if self._stream is not None else sys.stderr
|
||||
|
||||
def _should_update(self) -> bool:
|
||||
"""Check if enough time has passed for update."""
|
||||
now = time.time()
|
||||
@@ -259,15 +287,16 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
return f"{base_msg}: {message}"
|
||||
|
||||
def _safe_write(self, text: str) -> None:
|
||||
"""Safely write text to stdout handling encoding errors."""
|
||||
"""Safely write text to the progress stream handling encoding errors."""
|
||||
stream = self.stream
|
||||
try:
|
||||
sys.stdout.write(text)
|
||||
stream.write(text)
|
||||
except UnicodeEncodeError:
|
||||
# Fallback: encode with replacement and write decoded
|
||||
# Use ascii as safe fallback if encoding is unknown or caused error
|
||||
encoding = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
encoding = getattr(stream, "encoding", None) or "ascii"
|
||||
safe_text = text.encode(encoding, errors="replace").decode(encoding)
|
||||
sys.stdout.write(safe_text)
|
||||
stream.write(safe_text)
|
||||
|
||||
def update(self, item: ProgressItem) -> None:
|
||||
"""Update console progress display."""
|
||||
@@ -302,11 +331,11 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
self._display_item_line(pipeline_item)
|
||||
self._safe_write("\n")
|
||||
|
||||
sys.stdout.flush()
|
||||
self.stream.flush()
|
||||
else:
|
||||
# Original single-item display
|
||||
self._display_item_line(item)
|
||||
sys.stdout.flush()
|
||||
self.stream.flush()
|
||||
|
||||
def _display_item_line(self, item: ProgressItem) -> None:
|
||||
"""Display a single progress item line."""
|
||||
@@ -468,13 +497,13 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
f"Completed: {completed} | Failed: {failed} | Total Time: {total_time:.2f}s\n"
|
||||
)
|
||||
self._safe_write("=" * 80 + "\n")
|
||||
sys.stdout.flush()
|
||||
self.stream.flush()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear console display."""
|
||||
with self.lock:
|
||||
self._safe_write("\r" + " " * 100 + "\r")
|
||||
sys.stdout.flush()
|
||||
self.stream.flush()
|
||||
self.current_lines.clear()
|
||||
|
||||
|
||||
|
||||
@@ -66,12 +66,32 @@ class FAISSIndex:
|
||||
self.metadata: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def add_vectors(self, vectors: np.ndarray, ids: Optional[List[str]] = None):
|
||||
"""Add vectors to index."""
|
||||
"""
|
||||
Add vectors to index.
|
||||
|
||||
Skips any id already present in vector_ids rather than appending a
|
||||
second physical vector under the same id. FAISS indices here don't
|
||||
support removing or replacing a single vector in place, so an
|
||||
"update" isn't possible; without this check, re-running an add for
|
||||
ids that already exist (e.g. retrying an interrupted migration)
|
||||
would silently duplicate vectors under the same id on every retry.
|
||||
"""
|
||||
if ids is None:
|
||||
ids = [f"vec_{i}" for i in range(len(vectors))]
|
||||
|
||||
self.index.add(vectors.astype(np.float32))
|
||||
self.vector_ids.extend(ids)
|
||||
new_rows = []
|
||||
new_ids = []
|
||||
existing = set(self.vector_ids)
|
||||
for row, vec_id in zip(vectors, ids):
|
||||
if vec_id in existing:
|
||||
continue
|
||||
new_rows.append(row)
|
||||
new_ids.append(vec_id)
|
||||
existing.add(vec_id)
|
||||
|
||||
if new_rows:
|
||||
self.index.add(np.array(new_rows, dtype=np.float32))
|
||||
self.vector_ids.extend(new_ids)
|
||||
|
||||
def search(
|
||||
self, query_vectors: np.ndarray, k: int = 10
|
||||
@@ -305,6 +325,12 @@ class FAISSStore:
|
||||
"""
|
||||
Add vectors to index.
|
||||
|
||||
Any id that already exists in the index is skipped rather than
|
||||
stored as a second physical vector under the same id (see
|
||||
FAISSIndex.add_vectors), so calling this again with ids from a
|
||||
previous call is safe and doesn't accumulate duplicates. Metadata
|
||||
for those ids is still updated.
|
||||
|
||||
Args:
|
||||
vectors: List of vectors or numpy array
|
||||
ids: Vector IDs
|
||||
@@ -312,7 +338,8 @@ class FAISSStore:
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
List of vector IDs
|
||||
List of vector IDs (including ids that were already present
|
||||
and therefore not re-added as new vectors)
|
||||
"""
|
||||
num_vectors = len(vectors) if isinstance(vectors, (list, np.ndarray)) else 1
|
||||
tracking_id = self.progress_tracker.start_tracking(
|
||||
@@ -526,6 +553,30 @@ class FAISSStore:
|
||||
|
||||
return results
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors in insertion order.
|
||||
|
||||
Args:
|
||||
offset: Number of vectors to skip
|
||||
limit: Maximum number of vectors to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if self.index is None or limit <= 0:
|
||||
return []
|
||||
|
||||
ids_page = self.index.vector_ids[offset:offset + limit]
|
||||
return [
|
||||
{
|
||||
"id": vector_id,
|
||||
"metadata": self.get_metadata(vector_id) or {},
|
||||
"vector": self.get_vector(vector_id),
|
||||
}
|
||||
for vector_id in ids_page
|
||||
]
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get index statistics."""
|
||||
if self.index is None:
|
||||
|
||||
@@ -656,6 +656,52 @@ class PgVectorStore:
|
||||
self.logger.warning(f"Failed to get metadata for {vector_id}: {e}")
|
||||
return None
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors ordered by id.
|
||||
|
||||
Args:
|
||||
offset: Number of rows to skip
|
||||
limit: Maximum number of rows to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if not PSYCOPG3_AVAILABLE and not PSYCOPG2_AVAILABLE:
|
||||
raise ProcessingError(
|
||||
"Neither psycopg3 nor psycopg2 is available. "
|
||||
"Install with: pip install psycopg[binary] or psycopg2-binary"
|
||||
)
|
||||
|
||||
if limit <= 0:
|
||||
return []
|
||||
|
||||
scan_sql = psycopg_sql.SQL("""
|
||||
SELECT id, vector, metadata
|
||||
FROM {}
|
||||
ORDER BY id
|
||||
LIMIT %s OFFSET %s
|
||||
""").format(psycopg_sql.Identifier(self.table_name))
|
||||
|
||||
with self._get_connection() as conn:
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(scan_sql, (limit, offset))
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
vec_id, vec, meta = row
|
||||
results.append({
|
||||
"id": vec_id,
|
||||
"metadata": meta if isinstance(meta, dict) else json.loads(meta) if meta else {},
|
||||
"vector": np.array(vec) if vec is not None else None,
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
raise ProcessingError(f"Failed to scan vectors: {str(e)}") from e
|
||||
|
||||
def filter_by_metadata(
|
||||
self, filters: Dict[str, Any], limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -616,6 +616,49 @@ class SQLiteVecStore:
|
||||
self.logger.warning(f"Failed to get metadata for {vector_id}: {e}")
|
||||
return None
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors ordered by id.
|
||||
|
||||
Args:
|
||||
offset: Number of rows to skip
|
||||
limit: Maximum number of rows to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if limit <= 0:
|
||||
return []
|
||||
|
||||
query_sql = f"""
|
||||
SELECT id, embedding, metadata
|
||||
FROM {self.table_name}
|
||||
ORDER BY id
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
with self._lock, self._get_connection() as conn:
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(query_sql, (limit, offset))
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
vec_id, embedding_blob, meta_json = row
|
||||
vec = None
|
||||
if embedding_blob:
|
||||
vec = np.frombuffer(embedding_blob, dtype=np.float32).copy()
|
||||
results.append({
|
||||
"id": vec_id,
|
||||
"metadata": json.loads(meta_json) if meta_json else {},
|
||||
"vector": vec,
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
raise ProcessingError(f"Failed to scan vectors: {str(e)}") from e
|
||||
|
||||
def filter_by_metadata(
|
||||
self, filters: Dict[str, Any], limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -824,6 +824,64 @@ class VectorStore:
|
||||
else:
|
||||
raise NotImplementedError(f"Backend store {type(self._backend_store).__name__} does not implement get_metadata")
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors, backend-agnostic.
|
||||
|
||||
Follows the get_vector()/get_metadata() precedent (#843): the inmemory
|
||||
backend pages its local dict directly, a persistent backend delegates
|
||||
to a scan_vectors() on the wrapped store when available, and one that
|
||||
cannot enumerate its contents raises NotImplementedError rather than
|
||||
silently returning an empty page.
|
||||
|
||||
Args:
|
||||
offset: Number of vectors to skip
|
||||
limit: Maximum number of vectors to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if limit <= 0:
|
||||
return []
|
||||
|
||||
if self.backend == "inmemory":
|
||||
ids_page = list(self.vectors.keys())[offset:offset + limit]
|
||||
return [
|
||||
{
|
||||
"id": vec_id,
|
||||
"metadata": self.metadata.get(vec_id, {}),
|
||||
"vector": self.vectors.get(vec_id),
|
||||
}
|
||||
for vec_id in ids_page
|
||||
]
|
||||
elif self._backend_store and hasattr(self._backend_store, "scan_vectors"):
|
||||
return self._backend_store.scan_vectors(offset=offset, limit=limit)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Backend store {type(self._backend_store).__name__} does not "
|
||||
"implement scan_vectors(). Add a scan_vectors() method to the "
|
||||
"backend store adapter to enable enumeration for this backend."
|
||||
)
|
||||
|
||||
def iter_vectors(self, batch_size: int = 500):
|
||||
"""
|
||||
Iterate over every stored vector, one page at a time.
|
||||
|
||||
Args:
|
||||
batch_size: Number of vectors to fetch per underlying scan_vectors() call
|
||||
|
||||
Yields:
|
||||
Result dicts with 'id', 'metadata', and 'vector', in scan order
|
||||
"""
|
||||
offset = 0
|
||||
while True:
|
||||
page = self.scan_vectors(offset=offset, limit=batch_size)
|
||||
if not page:
|
||||
return
|
||||
for item in page:
|
||||
yield item
|
||||
offset += len(page)
|
||||
|
||||
def count(self) -> int:
|
||||
"""Return the number of vectors in the store, backend-agnostic.
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""AgentMemory.find_by_entity returns all matches by default (#1018).
|
||||
|
||||
The previous default limit of 10 silently truncated results, making erasure
|
||||
workflows incomplete for entities with more than 10 memories: a caller
|
||||
computing "what references this entity" from a truncated page would leave
|
||||
the remainder live. The unbounded default is deliberate — an erasure check
|
||||
cannot paginate — while callers that want a page still pass an explicit
|
||||
limit. (Previously lived in tests/test_seed_manager.py; moved to the
|
||||
AgentMemory area per review.)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
from semantica.context.agent_memory import AgentMemory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memory_with_15():
|
||||
mem = AgentMemory()
|
||||
for i in range(15):
|
||||
mem.store(
|
||||
content=f"fact {i} about entity",
|
||||
entities=[{"id": "e1", "name": "Entity", "type": "thing"}],
|
||||
)
|
||||
return mem
|
||||
|
||||
|
||||
class TestFindByEntityLimit:
|
||||
def test_returns_all_matches_by_default(self, memory_with_15):
|
||||
results = memory_with_15.find_by_entity("e1")
|
||||
assert len(results) == 15, f"expected 15 (all), got {len(results)}"
|
||||
|
||||
def test_explicit_limit_still_works(self, memory_with_15):
|
||||
assert len(memory_with_15.find_by_entity("e1", limit=5)) == 5
|
||||
|
||||
def test_no_matches_returns_empty(self):
|
||||
assert AgentMemory().find_by_entity("nonexistent") == []
|
||||
@@ -5,14 +5,23 @@ This module tests the decision tracking data models including
|
||||
validation, serialization, and deserialization.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.context.decision_models import (
|
||||
Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain,
|
||||
validate_decision, validate_policy, serialize_decision, deserialize_decision,
|
||||
serialize_policy, deserialize_policy
|
||||
ApprovalChain,
|
||||
Decision,
|
||||
DecisionContext,
|
||||
Policy,
|
||||
PolicyException,
|
||||
Precedent,
|
||||
deserialize_decision,
|
||||
deserialize_policy,
|
||||
serialize_decision,
|
||||
serialize_policy,
|
||||
validate_decision,
|
||||
validate_policy,
|
||||
)
|
||||
|
||||
|
||||
@@ -452,5 +461,82 @@ class TestSerializationFunctions:
|
||||
assert deserialized.metadata == original_policy.metadata
|
||||
|
||||
|
||||
class TestAutoGenerateIdContract:
|
||||
"""Test the auto_generate_id InitVar contract across all decision models.
|
||||
|
||||
Regression coverage for the InitVar fix: previously ``auto_generate_id``
|
||||
was a plain ``__post_init__`` parameter that dataclass-generated ``__init__``
|
||||
never forwarded, so the ``auto_generate_id=False`` branch was dead code and
|
||||
the "id is required" contract could never fire.
|
||||
"""
|
||||
|
||||
def _base_kwargs(self, cls):
|
||||
now = datetime.now()
|
||||
return {
|
||||
Decision: dict(
|
||||
decision_id="", category="c", scenario="s", reasoning="r",
|
||||
outcome="o", confidence=0.5, timestamp=now, decision_maker="m",
|
||||
),
|
||||
DecisionContext: dict(
|
||||
context_id="", decision_id="d", entity_snapshots={}, risk_factors=[],
|
||||
),
|
||||
Policy: dict(
|
||||
policy_id="", name="n", description="d", rules={}, category="c",
|
||||
version="1", created_at=now, updated_at=now,
|
||||
),
|
||||
PolicyException: dict(
|
||||
exception_id="", decision_id="d", policy_id="p", reason="r",
|
||||
approver="a", approval_timestamp=now, justification="j",
|
||||
),
|
||||
Precedent: dict(
|
||||
precedent_id="", source_decision_id="d", similarity_score=0.5,
|
||||
relationship_type="same_policy",
|
||||
),
|
||||
ApprovalChain: dict(
|
||||
approval_id="", decision_id="d", approver="a",
|
||||
approval_method="email", approval_context="x", timestamp=now,
|
||||
),
|
||||
}[cls]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cls",
|
||||
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
||||
)
|
||||
def test_auto_generate_id_is_not_a_field(self, cls):
|
||||
"""auto_generate_id must stay an InitVar, never a real dataclass field."""
|
||||
import dataclasses
|
||||
|
||||
names = [f.name for f in dataclasses.fields(cls)]
|
||||
assert "auto_generate_id" not in names
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cls",
|
||||
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
||||
)
|
||||
def test_default_auto_generates_id(self, cls):
|
||||
"""With defaults, an empty id is auto-populated and stays a non-field."""
|
||||
obj = cls(**self._base_kwargs(cls))
|
||||
id_field = [f.name for f in __import__("dataclasses").fields(cls)][0]
|
||||
assert getattr(obj, id_field)
|
||||
assert "auto_generate_id" not in vars(obj)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cls",
|
||||
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
||||
)
|
||||
def test_required_id_when_auto_generate_disabled(self, cls):
|
||||
"""auto_generate_id=False with an empty id must raise ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
cls(auto_generate_id=False, **self._base_kwargs(cls))
|
||||
|
||||
def test_explicit_id_honored_with_auto_generate_disabled(self):
|
||||
"""A provided id is preserved when auto_generate_id=False."""
|
||||
kwargs = self._base_kwargs(Decision)
|
||||
kwargs["decision_id"] = "fixed-id"
|
||||
decision = Decision(auto_generate_id=False, **kwargs)
|
||||
assert decision.decision_id == "fixed-id"
|
||||
assert "auto_generate_id" not in decision.to_dict()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
||||
@@ -291,8 +291,8 @@ class TestDeduplication(unittest.TestCase):
|
||||
class TestProgressTrackerEncoding(unittest.TestCase):
|
||||
"""Regression tests for issue #531 — Unicode crash on cp1252 Windows consoles."""
|
||||
|
||||
def _make_cp1252_stdout(self):
|
||||
"""Return a stdout-like object that raises UnicodeEncodeError for non-cp1252 chars."""
|
||||
def _make_cp1252_stream(self):
|
||||
"""Return a stream that raises UnicodeEncodeError for non-cp1252 chars."""
|
||||
class CP1252Writer:
|
||||
encoding = "cp1252"
|
||||
def write(self, text):
|
||||
@@ -304,39 +304,39 @@ class TestProgressTrackerEncoding(unittest.TestCase):
|
||||
def test_safe_write_does_not_crash_on_cp1252(self):
|
||||
"""_safe_write must not raise UnicodeEncodeError on a cp1252 console."""
|
||||
display = ConsoleProgressDisplay()
|
||||
orig = sys.stdout
|
||||
sys.stdout = self._make_cp1252_stdout()
|
||||
orig = sys.stderr
|
||||
sys.stderr = self._make_cp1252_stream()
|
||||
try:
|
||||
display._safe_write("🧠 Semantica - 📊 Current Progress\n")
|
||||
except UnicodeEncodeError:
|
||||
self.fail("_safe_write raised UnicodeEncodeError on cp1252 stdout")
|
||||
self.fail("_safe_write raised UnicodeEncodeError on a cp1252 stream")
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
sys.stderr = orig
|
||||
|
||||
def test_update_pipeline_header_does_not_crash_on_cp1252(self):
|
||||
"""update() pipeline header write must not crash on a cp1252 console (issue #531)."""
|
||||
from semantica.utils.progress_tracker import ProgressItem
|
||||
display = ConsoleProgressDisplay()
|
||||
display.use_emoji = True # force emoji path to exercise the fixed branch
|
||||
orig = sys.stdout
|
||||
sys.stdout = self._make_cp1252_stdout()
|
||||
orig = sys.stderr
|
||||
sys.stderr = self._make_cp1252_stream()
|
||||
try:
|
||||
display._safe_write("🧠 Semantica - 📊 Current Progress\n")
|
||||
display._safe_write("=" * 150 + "\n")
|
||||
except UnicodeEncodeError:
|
||||
self.fail("Pipeline header write raised UnicodeEncodeError on cp1252 stdout")
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
sys.stderr = orig
|
||||
|
||||
def test_emoji_detection_disables_on_cp1252(self):
|
||||
"""ConsoleProgressDisplay should auto-disable emoji when stdout is cp1252."""
|
||||
orig = sys.stdout
|
||||
sys.stdout = self._make_cp1252_stdout()
|
||||
"""ConsoleProgressDisplay should auto-disable emoji when the progress stream is cp1252."""
|
||||
orig = sys.stderr
|
||||
sys.stderr = self._make_cp1252_stream()
|
||||
try:
|
||||
display = ConsoleProgressDisplay()
|
||||
self.assertFalse(display.use_emoji, "use_emoji should be False on cp1252 stdout")
|
||||
self.assertFalse(display.use_emoji, "use_emoji should be False on a cp1252 progress stream")
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
sys.stderr = orig
|
||||
|
||||
|
||||
class TestResultLimiting(unittest.TestCase):
|
||||
|
||||
@@ -24,14 +24,19 @@ import math
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the imports below, which need that extra.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.routes.decisions import _node_to_decision
|
||||
from semantica.explorer.schemas import DecisionResponse
|
||||
from semantica.explorer.session import GraphSession
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
from pydantic import ValidationError # noqa: E402
|
||||
|
||||
from semantica.context.context_graph import ContextGraph # noqa: E402
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.routes.decisions import _node_to_decision # noqa: E402
|
||||
from semantica.explorer.schemas import DecisionResponse # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -9,16 +9,15 @@ import networkx as nx
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
|
||||
|
||||
|
||||
@@ -1104,7 +1103,7 @@ class TestBidirectionalPathRoute:
|
||||
# _classify_distance unit tests — issue #472
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from semantica.utils.helpers import classify_path_distance
|
||||
from semantica.utils.helpers import classify_path_distance # noqa: E402
|
||||
|
||||
|
||||
class _FakeSimilarity:
|
||||
|
||||
@@ -13,16 +13,15 @@ browsers can't set custom headers on a WebSocket handshake.
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
|
||||
|
||||
def _build_sample_graph() -> ContextGraph:
|
||||
|
||||
@@ -15,9 +15,14 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the explorer imports below, which pull fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.context.context_graph import ContextGraph # noqa: E402
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
@@ -27,7 +27,12 @@ import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.explorer.routes import ontology as ontology_mod
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.explorer.routes import ontology as ontology_mod # noqa: E402
|
||||
|
||||
|
||||
def _start_local_server():
|
||||
|
||||
@@ -21,7 +21,12 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.explorer.routes import ontology as ontology_mod
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.explorer.routes import ontology as ontology_mod # noqa: E402
|
||||
|
||||
|
||||
def _fake_getaddrinfo(host, *args, **kwargs):
|
||||
|
||||
@@ -6,17 +6,16 @@ from urllib.parse import quote
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.routes.ontology import OntologyEntry
|
||||
from semantica.explorer.session import GraphSession
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.routes.ontology import OntologyEntry # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
|
||||
|
||||
def _build_ontology_graph() -> ContextGraph:
|
||||
|
||||
@@ -5,13 +5,18 @@ Tests for ProvenanceManager wiring into Explorer routes and application startup.
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the starlette/explorer imports below, which need that extra.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
from semantica.provenance import ProvenanceManager
|
||||
from semantica.provenance.storage import SQLiteStorage
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
|
||||
from semantica.context.context_graph import ContextGraph # noqa: E402
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
from semantica.provenance import ProvenanceManager # noqa: E402
|
||||
from semantica.provenance.storage import SQLiteStorage # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from semantica.explorer.routes.provenance import (
|
||||
import pytest
|
||||
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.explorer.routes.provenance import ( # noqa: E402
|
||||
_add_chain_edges,
|
||||
_build_provenance,
|
||||
_render_markdown,
|
||||
|
||||
@@ -14,18 +14,17 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
import semantica.explorer.routes.sparql as sparql_mod
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
|
||||
import semantica.explorer.routes.sparql as sparql_mod # noqa: E402
|
||||
|
||||
|
||||
def _build_sample_graph() -> ContextGraph:
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from semantica.explorer.dependencies import get_session
|
||||
from semantica.explorer.routes.vocabulary import router
|
||||
from semantica.utils.skos import validate_skos_hierarchy
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from fastapi import FastAPI # noqa: E402
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
|
||||
from semantica.explorer.dependencies import get_session # noqa: E402
|
||||
from semantica.explorer.routes.vocabulary import router # noqa: E402
|
||||
from semantica.utils.skos import validate_skos_hierarchy # noqa: E402
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# tests/integrations/crewai package
|
||||
@@ -0,0 +1 @@
|
||||
# tests/integrations/langchain package
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Test for GraphBuilder with GraphStore backend (Issue #1135).
|
||||
|
||||
This test verifies that GraphBuilder correctly works with the GraphStore
|
||||
facade interface, not with raw backend stores like Neo4jStore.
|
||||
"""
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
class TestGraphBuilderWithGraphStore(unittest.TestCase):
|
||||
"""Test GraphBuilder integration with GraphStore facade."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Mock progress tracker
|
||||
self.mock_tracker_patcher = patch("semantica.utils.progress_tracker.get_progress_tracker")
|
||||
self.mock_get_tracker = self.mock_tracker_patcher.start()
|
||||
self.mock_tracker = MagicMock()
|
||||
self.mock_get_tracker.return_value = self.mock_tracker
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after tests."""
|
||||
self.mock_tracker_patcher.stop()
|
||||
|
||||
def test_graph_builder_with_graph_store_facade(self):
|
||||
"""Test that GraphBuilder works with GraphStore facade (Issue #1135)."""
|
||||
from semantica.kg.graph_builder import GraphBuilder
|
||||
from semantica.graph_store import GraphStore
|
||||
|
||||
# Create a mock GraphStore facade
|
||||
mock_store = MagicMock(spec=GraphStore)
|
||||
mock_store.add_nodes.return_value = 2
|
||||
mock_store.add_edges.return_value = 1
|
||||
|
||||
# Create GraphBuilder with the GraphStore facade
|
||||
builder = GraphBuilder(
|
||||
merge_entities=False,
|
||||
resolve_conflicts=False,
|
||||
graph_store=mock_store
|
||||
)
|
||||
|
||||
# Build a simple graph
|
||||
entities = [
|
||||
{"id": "alice", "type": "Person"},
|
||||
{"id": "bob", "type": "Person"},
|
||||
]
|
||||
relationships = [
|
||||
{"source": "alice", "target": "bob", "type": "knows"},
|
||||
]
|
||||
|
||||
graph = builder.build({
|
||||
"entities": entities,
|
||||
"relationships": relationships
|
||||
})
|
||||
|
||||
# Verify the graph was built
|
||||
self.assertEqual(len(graph["entities"]), 2)
|
||||
self.assertEqual(len(graph["relationships"]), 1)
|
||||
|
||||
# Verify that add_nodes and add_edges were called on the GraphStore
|
||||
mock_store.add_nodes.assert_called_once()
|
||||
mock_store.add_edges.assert_called_once()
|
||||
|
||||
def test_graph_builder_without_graph_store_still_works(self):
|
||||
"""Test that GraphBuilder still works without a graph_store parameter."""
|
||||
from semantica.kg.graph_builder import GraphBuilder
|
||||
|
||||
# Create GraphBuilder without graph_store
|
||||
builder = GraphBuilder(
|
||||
merge_entities=False,
|
||||
resolve_conflicts=False
|
||||
)
|
||||
|
||||
# Build a simple graph
|
||||
entities = [
|
||||
{"id": "alice", "type": "Person"},
|
||||
{"id": "bob", "type": "Person"},
|
||||
]
|
||||
relationships = [
|
||||
{"source": "alice", "target": "bob", "type": "knows"},
|
||||
]
|
||||
|
||||
graph = builder.build({
|
||||
"entities": entities,
|
||||
"relationships": relationships
|
||||
})
|
||||
|
||||
# Verify the graph was built
|
||||
self.assertEqual(len(graph["entities"]), 2)
|
||||
self.assertEqual(len(graph["relationships"]), 1)
|
||||
self.assertEqual(graph["metadata"]["num_entities"], 2)
|
||||
self.assertEqual(graph["metadata"]["num_relationships"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,366 @@
|
||||
"""Tests for the RETE engine pattern matching (issue #300).
|
||||
|
||||
These tests verify that ``AlphaNode._matches`` and ``BetaNode._can_join`` no
|
||||
longer behave like the old always-``True`` stubs, and that the network as a
|
||||
whole only fires rules whose conditions actually unify with the facts.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import re
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from semantica.reasoning import rete_engine
|
||||
from semantica.reasoning.reasoner import Fact, Rule
|
||||
from semantica.reasoning.rete_engine import (
|
||||
AlphaNode,
|
||||
BetaNode,
|
||||
ReteEngine,
|
||||
unify_condition,
|
||||
)
|
||||
|
||||
|
||||
class TestUnifyCondition(unittest.TestCase):
|
||||
def test_single_variable_binds(self):
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
bindings = unify_condition("Person(?x)", fact)
|
||||
self.assertEqual(bindings, {"x": "John"})
|
||||
|
||||
def test_predicate_mismatch_returns_none(self):
|
||||
fact = Fact("f1", "Company", ["Google"])
|
||||
self.assertIsNone(unify_condition("Person(?x)", fact))
|
||||
|
||||
def test_two_arguments_bind(self):
|
||||
fact = Fact("f2", "Parent", ["John", "Mary"])
|
||||
bindings = unify_condition("Parent(?x, ?y)", fact)
|
||||
self.assertEqual(bindings, {"x": "John", "y": "Mary"})
|
||||
|
||||
def test_literal_argument_must_match(self):
|
||||
fact = Fact("f3", "Parent", ["John", "Mary"])
|
||||
self.assertIsNone(unify_condition("Parent(Bob, ?y)", fact))
|
||||
self.assertEqual(unify_condition("Parent(John, ?y)", fact), {"y": "Mary"})
|
||||
|
||||
def test_repeated_variable_requires_equal_values(self):
|
||||
loves_self = Fact("f4", "Loves", ["John", "John"])
|
||||
loves_other = Fact("f5", "Loves", ["John", "Mary"])
|
||||
self.assertEqual(unify_condition("Loves(?x, ?x)", loves_self), {"x": "John"})
|
||||
self.assertIsNone(unify_condition("Loves(?x, ?x)", loves_other))
|
||||
|
||||
def test_regex_error_logs_warning_and_returns_none(self):
|
||||
"""A regex compilation error is logged with context and yields None."""
|
||||
fact = Fact("f6", "Person", ["John"])
|
||||
with mock.patch.object(
|
||||
rete_engine.re,
|
||||
"match",
|
||||
side_effect=re.error("bad pattern"),
|
||||
), self.assertLogs("semantica.rete_engine", level="WARNING") as captured:
|
||||
result = unify_condition("Person(?x)", fact)
|
||||
self.assertIsNone(result)
|
||||
joined = "\n".join(captured.output)
|
||||
self.assertIn("Person(?x)", joined)
|
||||
self.assertIn("Person(John)", joined)
|
||||
self.assertIn("bad pattern", joined)
|
||||
|
||||
def test_unexpected_error_logs_warning_and_returns_none(self):
|
||||
"""An unexpected error is also logged and swallowed as None."""
|
||||
fact = Fact("f7", "Person", ["John"])
|
||||
with mock.patch.object(
|
||||
rete_engine.re,
|
||||
"match",
|
||||
side_effect=RuntimeError("boom"),
|
||||
), self.assertLogs("semantica.rete_engine", level="WARNING") as captured:
|
||||
result = unify_condition("Person(?x)", fact)
|
||||
self.assertIsNone(result)
|
||||
self.assertIn("boom", "\n".join(captured.output))
|
||||
|
||||
|
||||
class TestAlphaNode(unittest.TestCase):
|
||||
def test_matches_stores_bindings(self):
|
||||
node = AlphaNode("a1", "Person(?x)")
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
token = node.add_fact(fact)
|
||||
self.assertIsNotNone(token)
|
||||
assert token is not None # narrow type for the checker
|
||||
self.assertEqual(token.facts, [fact])
|
||||
self.assertEqual(token.bindings, {"x": "John"})
|
||||
self.assertIn(token, node.tokens)
|
||||
|
||||
def test_non_matching_fact_rejected(self):
|
||||
node = AlphaNode("a1", "Person(?x)")
|
||||
fact = Fact("f1", "Company", ["Google"])
|
||||
self.assertIsNone(node.add_fact(fact))
|
||||
self.assertEqual(node.tokens, [])
|
||||
|
||||
def test_uses_precompiled_regex(self):
|
||||
"""AlphaNode compiles its condition once and reuses it per fact."""
|
||||
node = AlphaNode("a1", "Person(?x)")
|
||||
self.assertIsNotNone(node._compiled)
|
||||
# Matching goes through the compiled matcher, not unify_condition.
|
||||
with mock.patch.object(rete_engine, "unify_condition") as unify:
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
token = node.add_fact(fact)
|
||||
unify.assert_not_called()
|
||||
self.assertIsNotNone(token)
|
||||
assert token is not None
|
||||
self.assertEqual(token.bindings, {"x": "John"})
|
||||
|
||||
def test_bad_condition_never_matches_and_logs(self):
|
||||
"""A condition that fails to compile logs a warning and never fires."""
|
||||
with mock.patch.object(
|
||||
rete_engine,
|
||||
"_build_condition_regex",
|
||||
return_value="(unbalanced",
|
||||
), self.assertLogs("semantica.rete_engine", level="WARNING") as captured:
|
||||
node = AlphaNode("bad", "Person(?x)")
|
||||
self.assertIsNone(node._compiled)
|
||||
self.assertIn("failed to compile", "\n".join(captured.output))
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
self.assertIsNone(node.add_fact(fact))
|
||||
self.assertEqual(node.tokens, [])
|
||||
|
||||
|
||||
class TestBetaNode(unittest.TestCase):
|
||||
def test_join_consistent_bindings(self):
|
||||
left = AlphaNode("a1", "Parent(?x, ?y)")
|
||||
right = AlphaNode("a2", "Person(?x)")
|
||||
beta = BetaNode("b1", left, right)
|
||||
|
||||
parent = Fact("f1", "Parent", ["John", "Mary"])
|
||||
person = Fact("f2", "Person", ["John"])
|
||||
left_token = left.add_fact(parent)
|
||||
right_token = right.add_fact(person)
|
||||
assert left_token is not None and right_token is not None
|
||||
|
||||
merged = beta.join(left_token, right_token)
|
||||
self.assertIsNotNone(merged)
|
||||
assert merged is not None # narrow type for the checker
|
||||
self.assertEqual(merged.bindings, {"x": "John", "y": "Mary"})
|
||||
# Facts are concatenated left-then-right in condition order.
|
||||
self.assertEqual(merged.facts, [parent, person])
|
||||
|
||||
def test_join_conflicting_bindings_rejected(self):
|
||||
left = AlphaNode("a1", "Parent(?x, ?y)")
|
||||
right = AlphaNode("a2", "Person(?x)")
|
||||
beta = BetaNode("b1", left, right)
|
||||
|
||||
parent = Fact("f1", "Parent", ["John", "Mary"])
|
||||
# ?x conflicts: John vs Alice
|
||||
person = Fact("f2", "Person", ["Alice"])
|
||||
left_token = left.add_fact(parent)
|
||||
right_token = right.add_fact(person)
|
||||
assert left_token is not None and right_token is not None
|
||||
|
||||
self.assertIsNone(beta.join(left_token, right_token))
|
||||
|
||||
|
||||
class TestReteEngineEndToEnd(unittest.TestCase):
|
||||
def test_only_matching_rule_fires(self):
|
||||
engine = ReteEngine()
|
||||
rule = Rule(
|
||||
rule_id="r1",
|
||||
name="person rule",
|
||||
conditions=["Person(?x)"],
|
||||
conclusion="Mortal(?x)",
|
||||
)
|
||||
engine.build_network([rule])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Company", ["Google"])) # should NOT fire
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0].bindings, {"x": "John"})
|
||||
|
||||
def test_multi_condition_join(self):
|
||||
engine = ReteEngine()
|
||||
rule = Rule(
|
||||
rule_id="r1",
|
||||
name="child rule",
|
||||
conditions=["Person(?x)", "Parent(?x, ?y)"],
|
||||
conclusion="Child(?y, ?x)",
|
||||
)
|
||||
engine.build_network([rule])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
# Unrelated parent whose ?x does not match any Person -> no activation.
|
||||
engine.add_fact(Fact("f3", "Parent", ["Bob", "Sue"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0].bindings, {"x": "John", "y": "Mary"})
|
||||
|
||||
def test_no_activation_when_join_inconsistent(self):
|
||||
engine = ReteEngine()
|
||||
rule = Rule(
|
||||
rule_id="r1",
|
||||
name="child rule",
|
||||
conditions=["Person(?x)", "Parent(?x, ?y)"],
|
||||
conclusion="Child(?y, ?x)",
|
||||
)
|
||||
engine.build_network([rule])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["Alice", "Mary"])) # ?x mismatch
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(matches, [])
|
||||
|
||||
|
||||
class TestThreeConditionChain(unittest.TestCase):
|
||||
"""Chained beta joins across three or more conditions (issue #300).
|
||||
|
||||
These exercise the Token model: a token must accumulate the ordered
|
||||
facts and the consistent bindings of every condition, so that deep
|
||||
chains neither drop bindings nor duplicate facts, and a conflict on the
|
||||
third condition correctly suppresses activation.
|
||||
"""
|
||||
|
||||
def _three_condition_rule(self):
|
||||
return Rule(
|
||||
rule_id="r1",
|
||||
name="location chain",
|
||||
conditions=[
|
||||
"Person(?x)",
|
||||
"Parent(?x, ?y)",
|
||||
"Located(?y, ?z)",
|
||||
],
|
||||
conclusion="LivesNear(?x, ?z)",
|
||||
)
|
||||
|
||||
def test_three_condition_valid_match(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
engine.add_fact(Fact("f3", "Located", ["Mary", "Paris"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(
|
||||
matches[0].bindings,
|
||||
{"x": "John", "y": "Mary", "z": "Paris"},
|
||||
)
|
||||
|
||||
def test_three_condition_third_level_conflict(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
# ?y is bound to Mary, so a Located fact about Bob must not join.
|
||||
engine.add_fact(Fact("f3", "Located", ["Bob", "Paris"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(matches, [])
|
||||
|
||||
def test_fact_insertion_order_independent(self):
|
||||
# Whatever order facts arrive, the same single match must result.
|
||||
base_facts = [
|
||||
Fact("f1", "Person", ["John"]),
|
||||
Fact("f2", "Parent", ["John", "Mary"]),
|
||||
Fact("f3", "Located", ["Mary", "Paris"]),
|
||||
]
|
||||
expected = {"x": "John", "y": "Mary", "z": "Paris"}
|
||||
|
||||
for order in itertools.permutations(base_facts):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
for fact in order:
|
||||
engine.add_fact(fact)
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1, f"order={order}")
|
||||
self.assertEqual(matches[0].bindings, expected)
|
||||
|
||||
def test_match_facts_complete_in_condition_order(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
person = Fact("f1", "Person", ["John"])
|
||||
parent = Fact("f2", "Parent", ["John", "Mary"])
|
||||
located = Fact("f3", "Located", ["Mary", "Paris"])
|
||||
engine.add_fact(person)
|
||||
engine.add_fact(parent)
|
||||
engine.add_fact(located)
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
# All three facts preserved, in condition order, no duplicates.
|
||||
self.assertEqual(matches[0].facts, [person, parent, located])
|
||||
|
||||
def test_multiple_left_tokens_join_one_right_fact(self):
|
||||
# Two Person/Parent chains sharing the same Located(?y, ?z) fact.
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
engine.add_fact(Fact("f3", "Person", ["Alice"]))
|
||||
engine.add_fact(Fact("f4", "Parent", ["Alice", "Mary"]))
|
||||
# One right fact should join with both accumulated left tokens.
|
||||
engine.add_fact(Fact("f5", "Located", ["Mary", "Paris"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 2)
|
||||
result = {m.bindings["x"]: m.bindings["z"] for m in matches}
|
||||
self.assertEqual(result, {"John": "Paris", "Alice": "Paris"})
|
||||
|
||||
def test_matches_reasoner_match_rule(self):
|
||||
from semantica.reasoning.reasoner import Reasoner
|
||||
|
||||
rule = self._three_condition_rule()
|
||||
facts = [
|
||||
Fact("f1", "Person", ["John"]),
|
||||
Fact("f2", "Parent", ["John", "Mary"]),
|
||||
Fact("f3", "Located", ["Mary", "Paris"]),
|
||||
]
|
||||
|
||||
# Reasoner works over stringified facts and returns
|
||||
# (conclusion, matched_facts, bindings) tuples from self.facts.
|
||||
reasoner = Reasoner()
|
||||
for fact in facts:
|
||||
reasoner.add_fact(str(fact))
|
||||
reasoner_matches = reasoner._match_rule(rule)
|
||||
|
||||
engine = ReteEngine()
|
||||
engine.build_network([rule])
|
||||
for fact in facts:
|
||||
engine.add_fact(fact)
|
||||
rete_matches = engine.match_patterns()
|
||||
|
||||
# Both engines must agree on the number of activations.
|
||||
self.assertEqual(len(rete_matches), len(reasoner_matches))
|
||||
self.assertEqual(len(rete_matches), 1)
|
||||
self.assertEqual(
|
||||
rete_matches[0].bindings,
|
||||
{"x": "John", "y": "Mary", "z": "Paris"},
|
||||
)
|
||||
# The RETE match must carry the instantiated conclusion facts too.
|
||||
conclusion, _, _ = reasoner_matches[0]
|
||||
self.assertEqual(conclusion, "LivesNear(John, Paris)")
|
||||
|
||||
def test_reset_clears_all_token_memory(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
engine.add_fact(Fact("f3", "Located", ["Mary", "Paris"]))
|
||||
self.assertEqual(len(engine.match_patterns()), 1)
|
||||
|
||||
engine.reset()
|
||||
|
||||
# No stale facts, tokens or activations remain anywhere.
|
||||
self.assertEqual(engine.facts, [])
|
||||
for node in engine.network.values():
|
||||
if isinstance(node, AlphaNode):
|
||||
self.assertEqual(node.tokens, [])
|
||||
elif isinstance(node, BetaNode):
|
||||
self.assertEqual(node.left_tokens, [])
|
||||
self.assertEqual(node.right_tokens, [])
|
||||
self.assertEqual(engine.match_patterns(), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -60,6 +60,11 @@ class TestSlidingWindowChunker:
|
||||
with pytest.raises(ValidationError):
|
||||
SlidingWindowChunker(chunk_size=100, overlap=100)
|
||||
|
||||
@pytest.mark.parametrize("stride", [0, -1])
|
||||
def test_init_rejects_non_positive_stride(self, stride):
|
||||
with pytest.raises(ValidationError, match="stride must be positive"):
|
||||
SlidingWindowChunker(chunk_size=100, stride=stride)
|
||||
|
||||
def test_empty_text_returns_empty(self):
|
||||
chunker = SlidingWindowChunker(chunk_size=50, overlap=10)
|
||||
assert chunker.chunk("") == []
|
||||
@@ -97,6 +102,49 @@ class TestSlidingWindowChunker:
|
||||
chunks = chunker.chunk_with_overlap(text, overlap_size=15)
|
||||
assert len(chunks) >= 2
|
||||
assert chunker.overlap == 0
|
||||
assert chunker.stride == 50
|
||||
|
||||
@pytest.mark.parametrize("overlap_size", [-1, 50, 51])
|
||||
def test_chunk_with_overlap_rejects_invalid_override(self, overlap_size):
|
||||
chunker = SlidingWindowChunker(chunk_size=50)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
chunker.chunk_with_overlap(
|
||||
"non-empty input", overlap_size=overlap_size
|
||||
)
|
||||
|
||||
def test_chunk_with_overlap_accepts_largest_valid_override(self):
|
||||
chunker = SlidingWindowChunker(chunk_size=5)
|
||||
|
||||
chunks = chunker.chunk_with_overlap("abcdefghij", overlap_size=4)
|
||||
|
||||
assert [chunk.start_index for chunk in chunks] == list(range(10))
|
||||
|
||||
def test_chunk_with_overlap_restores_custom_stride(self):
|
||||
chunker = SlidingWindowChunker(chunk_size=10, overlap=2, stride=3)
|
||||
|
||||
chunker.chunk_with_overlap(
|
||||
"abcdefghijklmnopqrstuvwxyz", overlap_size=4
|
||||
)
|
||||
|
||||
assert chunker.overlap == 2
|
||||
assert chunker.stride == 3
|
||||
|
||||
def test_chunk_with_overlap_restores_state_when_chunk_raises(
|
||||
self, monkeypatch
|
||||
):
|
||||
chunker = SlidingWindowChunker(chunk_size=10, overlap=2, stride=3)
|
||||
|
||||
def raise_error(text):
|
||||
raise RuntimeError("chunk failed")
|
||||
|
||||
monkeypatch.setattr(chunker, "chunk", raise_error)
|
||||
|
||||
with pytest.raises(RuntimeError, match="chunk failed"):
|
||||
chunker.chunk_with_overlap("non-empty input", overlap_size=4)
|
||||
|
||||
assert chunker.overlap == 2
|
||||
assert chunker.stride == 3
|
||||
|
||||
def test_boundary_preservation_avoids_mid_word_when_possible(self):
|
||||
text = (
|
||||
|
||||
@@ -1359,6 +1359,101 @@ class TestStore:
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate", "--from", "faiss"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_migrate_refuses_unsupported_backend_pair(self, runner):
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "faiss", "--to", "qdrant"])
|
||||
assert result.exit_code != 0
|
||||
assert "faiss, pgvector, sqlite" in result.output
|
||||
|
||||
def _fake_migrate_store_module(self, source_items, stored, dest_configs=None):
|
||||
class _FakeBackendStore:
|
||||
def __init__(self, dimension=None):
|
||||
self.dimension = dimension
|
||||
|
||||
class _FakeStore:
|
||||
def __init__(self, backend, config=None, **kw):
|
||||
self.backend = backend
|
||||
self._config = config or {}
|
||||
dim = self._config.get("dimension")
|
||||
self._backend_store = _FakeBackendStore(dimension=dim)
|
||||
if dest_configs is not None:
|
||||
dest_configs[backend] = dict(self._config)
|
||||
|
||||
def iter_vectors(self, batch_size=500):
|
||||
if self.backend == "sqlite":
|
||||
yield from source_items
|
||||
return
|
||||
return
|
||||
yield # pragma: no cover - makes this a generator for other backends
|
||||
|
||||
def store_vectors(self, vectors, metadata, ids=None):
|
||||
for vec_id, meta in zip(ids, metadata):
|
||||
stored[vec_id] = meta
|
||||
|
||||
return _fake_module(VectorStore=_FakeStore)
|
||||
|
||||
def test_migrate_runs_between_supported_backends(self, runner, monkeypatch):
|
||||
source_items = [
|
||||
{"id": "a", "vector": [0.1, 0.2], "metadata": {"tag": "x"}},
|
||||
{"id": "b", "vector": [0.3, 0.4], "metadata": {}},
|
||||
]
|
||||
stored = {}
|
||||
fake_vs = self._fake_migrate_store_module(source_items, stored)
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "pgvector",
|
||||
"--namespace", "prod", "--json"])
|
||||
_ok(result)
|
||||
data = _json_output(result)
|
||||
assert data == {"from": "sqlite", "to": "pgvector", "migrated": 2}
|
||||
assert stored == {"a": {"tag": "x", "namespace": "prod"}, "b": {"namespace": "prod"}}
|
||||
|
||||
def test_migrate_reports_zero_for_empty_source(self, runner, monkeypatch):
|
||||
stored = {}
|
||||
fake_vs = self._fake_migrate_store_module([], stored)
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "pgvector", "--json"])
|
||||
_ok(result)
|
||||
assert _json_output(result)["migrated"] == 0
|
||||
assert stored == {}
|
||||
|
||||
def test_migrate_inherits_source_dimension_into_dest(self, runner, monkeypatch):
|
||||
source_items = [{"id": "a", "vector": [0.1, 0.2, 0.3], "metadata": {}}]
|
||||
stored = {}
|
||||
dest_configs: dict = {}
|
||||
fake_vs = self._fake_migrate_store_module(source_items, stored, dest_configs)
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
monkeypatch.setattr(
|
||||
cli_module.Config, "to_dict",
|
||||
lambda self: {"vector_store": {"sqlite": {"dimension": 3}, "pgvector": {}}},
|
||||
)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "pgvector", "--json"])
|
||||
_ok(result)
|
||||
assert dest_configs["pgvector"].get("dimension") == 3
|
||||
|
||||
def test_migrate_faiss_source_requires_index_path(self, runner, monkeypatch):
|
||||
fake_vs = _fake_module(VectorStore=lambda **kw: MagicMock())
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "faiss", "--to", "sqlite"])
|
||||
assert result.exit_code != 0
|
||||
assert "index_path" in result.output
|
||||
|
||||
def test_migrate_faiss_dest_requires_index_path(self, runner, monkeypatch):
|
||||
fake_vs = _fake_module(VectorStore=lambda **kw: MagicMock())
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "faiss"])
|
||||
assert result.exit_code != 0
|
||||
assert "index_path" in result.output
|
||||
|
||||
def test_flush_requires_confirm(self, runner):
|
||||
result = runner.invoke(cli_module.main, ["store", "flush"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Tests for the Anthropic LLM provider wrapper (semantica.llms.Anthropic)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Anthropic
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
"""Anthropic(...) should not crash and should remember what it was given."""
|
||||
claude = Anthropic(model="claude-sonnet-4-6", api_key="fake-key")
|
||||
assert claude.model == "claude-sonnet-4-6"
|
||||
assert claude.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
"""Without a real key, is_available() must be a real False, not truthy junk.
|
||||
|
||||
api_key=None alone isn't enough to prove this: AnthropicProvider falls
|
||||
back to the ANTHROPIC_API_KEY environment variable, so this test has to
|
||||
clear it too or it would pass/fail depending on whoever's machine or CI
|
||||
runner happens to run it.
|
||||
"""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
assert claude.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
"""generate() must fail loudly.
|
||||
|
||||
Clears ANTHROPIC_API_KEY for the same reason as test_is_available_false_with_no_key:
|
||||
otherwise this test flakes depending on whether the runner's environment has a key set.
|
||||
"""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Anthropic provider not available"):
|
||||
claude.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
"""When available, generate() must actually call through to the real provider."""
|
||||
claude = Anthropic(api_key="fake-key")
|
||||
|
||||
claude.provider = MagicMock()
|
||||
claude.provider.is_available.return_value = True
|
||||
claude.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = claude.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
claude.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
claude = Anthropic(api_key="fake-key")
|
||||
claude.provider = MagicMock()
|
||||
claude.provider.is_available.return_value = True
|
||||
claude.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = claude.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
claude.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
claude = Anthropic(api_key="fake-key")
|
||||
claude.provider = MagicMock()
|
||||
claude.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
claude.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = claude.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
claude.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
"""generate_structured() must fail loudly, same as generate()."""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Anthropic provider not available"):
|
||||
claude.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
"""generate_typed() must fail loudly, same as generate() and generate_structured()."""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Anthropic provider not available"):
|
||||
claude.generate_typed("hello", object())
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for the DeepSeek LLM provider wrapper (semantica.llms.DeepSeek)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import DeepSeek
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
llm = DeepSeek(model="deepseek-chat", api_key="fake-key")
|
||||
assert llm.model == "deepseek-chat"
|
||||
assert llm.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
assert llm.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="DeepSeek provider not available"):
|
||||
llm.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
llm = DeepSeek(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = llm.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
llm.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
llm = DeepSeek(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = llm.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
llm.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
llm = DeepSeek(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
llm.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = llm.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
llm.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="DeepSeek provider not available"):
|
||||
llm.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="DeepSeek provider not available"):
|
||||
llm.generate_typed("hello", object())
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for the Gemini LLM provider wrapper (semantica.llms.Gemini)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Gemini
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
gemini = Gemini(model="gemini-pro", api_key="fake-key")
|
||||
assert gemini.model == "gemini-pro"
|
||||
assert gemini.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
assert gemini.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Gemini provider not available"):
|
||||
gemini.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
gemini = Gemini(api_key="fake-key")
|
||||
gemini.provider = MagicMock()
|
||||
gemini.provider.is_available.return_value = True
|
||||
gemini.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = gemini.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
gemini.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
gemini = Gemini(api_key="fake-key")
|
||||
gemini.provider = MagicMock()
|
||||
gemini.provider.is_available.return_value = True
|
||||
gemini.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = gemini.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
gemini.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
gemini = Gemini(api_key="fake-key")
|
||||
gemini.provider = MagicMock()
|
||||
gemini.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
gemini.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = gemini.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
gemini.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Gemini provider not available"):
|
||||
gemini.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Gemini provider not available"):
|
||||
gemini.generate_typed("hello", object())
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for the Novita LLM provider wrapper (semantica.llms.Novita)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Novita
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
llm = Novita(model="deepseek/deepseek-v3.2", api_key="fake-key")
|
||||
assert llm.model == "deepseek/deepseek-v3.2"
|
||||
assert llm.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
assert llm.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Novita provider not available"):
|
||||
llm.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
llm = Novita(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = llm.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
llm.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
llm = Novita(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = llm.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
llm.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
llm = Novita(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
llm.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = llm.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
llm.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Novita provider not available"):
|
||||
llm.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Novita provider not available"):
|
||||
llm.generate_typed("hello", object())
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Tests for the Ollama LLM provider wrapper (semantica.llms.Ollama)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Ollama
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_base_url():
|
||||
llm = Ollama(model="llama2", base_url="http://localhost:11434")
|
||||
assert llm.model == "llama2"
|
||||
assert llm.base_url == "http://localhost:11434"
|
||||
|
||||
|
||||
def test_is_available_false_without_a_running_server():
|
||||
"""No api_key here, Ollama has none. Without a real server (or the ollama
|
||||
package) reachable at base_url, this must be a real False."""
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
assert llm.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable():
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
with pytest.raises(ProcessingError, match="Ollama provider not available"):
|
||||
llm.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
llm = Ollama()
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = llm.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
llm.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
llm = Ollama()
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = llm.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
llm.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
llm = Ollama()
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
llm.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = llm.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
llm.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable():
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
with pytest.raises(ProcessingError, match="Ollama provider not available"):
|
||||
llm.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable():
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
with pytest.raises(ProcessingError, match="Ollama provider not available"):
|
||||
llm.generate_typed("hello", object())
|
||||
@@ -24,9 +24,20 @@ import pytest
|
||||
# rdf:/rdfs: namespaces) and neither the code nor this test caught it,
|
||||
# since both had the same bug. Importing the real function makes that class
|
||||
# of drift impossible.
|
||||
from semantica.explorer.routes.sparql import _is_read_only_query
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this import
|
||||
# fails on a plain dev install. Only the SPARQL class below needs it; the Cypher,
|
||||
# XXE, vector-serialization and SSRF classes in this module are independent, so
|
||||
# the skip is scoped to the one class rather than the whole file.
|
||||
try:
|
||||
from semantica.explorer.routes.sparql import _is_read_only_query
|
||||
except ImportError: # pragma: no cover - depends on the installed extras
|
||||
_is_read_only_query = None
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
_is_read_only_query is None,
|
||||
reason="requires semantica[explorer] (fastapi)",
|
||||
)
|
||||
class TestSparqlReadOnlyValidation:
|
||||
"""Regression tests for SPARQL injection prevention."""
|
||||
|
||||
|
||||
@@ -443,4 +443,3 @@ def test_export_seed_data(seed_manager, temp_data_dir):
|
||||
rows = list(reader)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["id"] == "1"
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Console progress must be written to stderr, never to stdout.
|
||||
|
||||
Progress is diagnostic output. Writing it to stdout corrupts any program that
|
||||
carries a machine-readable protocol there — the stdio MCP servers put
|
||||
newline-delimited JSON-RPC on stdout, and a progress bar interleaved with a
|
||||
response body makes that response unparseable (#1134).
|
||||
|
||||
Both servers currently defend against this by setting
|
||||
SEMANTICA_DISABLE_PROGRESS, and a console display is only attached when the
|
||||
console is interactive. Those are containments, not the fix: they give up
|
||||
progress output entirely, and every future entry point has to remember them.
|
||||
Writing to the correct stream in the first place is what these tests pin.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import io
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def progress_module():
|
||||
"""Import the real progress_tracker, bypassing a mocked sys.modules entry.
|
||||
|
||||
tests/test_extractors_dispatch.py assigns a MagicMock over
|
||||
'semantica.utils.progress_tracker' at import time and never restores it, so
|
||||
a plain module-level import here returns mocks when that file has already
|
||||
run. Dropping the cached entry re-imports the real module.
|
||||
|
||||
Both bindings are restored afterwards: importing a submodule also rebinds it
|
||||
as an attribute of its parent package, so restoring only the sys.modules
|
||||
entry would leave `semantica.utils.progress_tracker` and
|
||||
`sys.modules["semantica.utils.progress_tracker"]` pointing at different
|
||||
objects for every test that follows.
|
||||
"""
|
||||
name = "semantica.utils.progress_tracker"
|
||||
attr = name.rsplit(".", 1)[1]
|
||||
parent = importlib.import_module("semantica.utils")
|
||||
|
||||
missing = object()
|
||||
saved_entry = sys.modules.get(name, missing)
|
||||
saved_attr = getattr(parent, attr, missing)
|
||||
|
||||
sys.modules.pop(name, None)
|
||||
try:
|
||||
module = importlib.import_module(name)
|
||||
assert hasattr(module, "__file__"), "expected the real module, got a stand-in"
|
||||
yield module
|
||||
finally:
|
||||
if saved_entry is missing:
|
||||
sys.modules.pop(name, None)
|
||||
else:
|
||||
sys.modules[name] = saved_entry
|
||||
|
||||
if saved_attr is missing:
|
||||
if hasattr(parent, attr):
|
||||
delattr(parent, attr)
|
||||
else:
|
||||
setattr(parent, attr, saved_attr)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def display_cls(progress_module):
|
||||
return progress_module.ConsoleProgressDisplay
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_item(progress_module):
|
||||
def _make(**overrides):
|
||||
defaults = dict(
|
||||
module="kg",
|
||||
submodule="Reasoner",
|
||||
message="Inferring facts",
|
||||
status="running",
|
||||
total_items=10,
|
||||
processed_items=3,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
return progress_module.ProgressItem(**defaults)
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
class TestProgressStreamDefaults:
|
||||
def test_defaults_to_stderr(self, display_cls):
|
||||
assert display_cls().stream is sys.stderr
|
||||
|
||||
def test_default_is_not_stdout(self, display_cls):
|
||||
"""The whole point: stdout stays clean for protocol traffic."""
|
||||
assert display_cls().stream is not sys.stdout
|
||||
|
||||
def test_stream_follows_rebinding(self, display_cls, monkeypatch):
|
||||
"""Resolved per write, so pytest capture and later rebinds are honoured."""
|
||||
display = display_cls()
|
||||
replacement = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stderr", replacement)
|
||||
assert display.stream is replacement
|
||||
|
||||
def test_explicit_stream_overrides_the_default(self, display_cls):
|
||||
buffer = io.StringIO()
|
||||
assert display_cls(stream=buffer).stream is buffer
|
||||
|
||||
|
||||
class TestProgressWritesGoToTheStream:
|
||||
def test_update_writes_to_the_configured_stream(self, display_cls, make_item):
|
||||
buffer = io.StringIO()
|
||||
display = display_cls(stream=buffer, use_emoji=False, update_interval=0.0)
|
||||
|
||||
display.update(make_item())
|
||||
|
||||
assert buffer.getvalue(), "progress should have been rendered"
|
||||
|
||||
def test_update_writes_nothing_to_stdout(self, display_cls, make_item, monkeypatch):
|
||||
"""Regression guard for #1134: stdout must stay untouched."""
|
||||
fake_stdout = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stdout", fake_stdout)
|
||||
buffer = io.StringIO()
|
||||
|
||||
display = display_cls(stream=buffer, use_emoji=False, update_interval=0.0)
|
||||
display.update(make_item())
|
||||
display.clear()
|
||||
|
||||
assert fake_stdout.getvalue() == "", (
|
||||
f"console progress leaked to stdout: {fake_stdout.getvalue()!r}"
|
||||
)
|
||||
|
||||
def test_default_display_writes_nothing_to_stdout(
|
||||
self, display_cls, make_item, monkeypatch
|
||||
):
|
||||
"""Same guard without an explicit stream, i.e. the real default path."""
|
||||
fake_stdout = io.StringIO()
|
||||
fake_stderr = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stdout", fake_stdout)
|
||||
monkeypatch.setattr(sys, "stderr", fake_stderr)
|
||||
|
||||
display = display_cls(use_emoji=False, update_interval=0.0)
|
||||
display.update(make_item())
|
||||
|
||||
assert fake_stdout.getvalue() == ""
|
||||
assert fake_stderr.getvalue(), "progress should have gone to stderr"
|
||||
|
||||
def test_clear_flushes_the_stream_not_stdout(self, display_cls, monkeypatch):
|
||||
flushed = []
|
||||
|
||||
class RecordingStream(io.StringIO):
|
||||
def flush(self):
|
||||
flushed.append("stream")
|
||||
|
||||
class ExplodingStdout(io.StringIO):
|
||||
def flush(self): # pragma: no cover - fails the test if reached
|
||||
raise AssertionError("progress must not flush stdout")
|
||||
|
||||
monkeypatch.setattr(sys, "stdout", ExplodingStdout())
|
||||
display = display_cls(
|
||||
stream=RecordingStream(), use_emoji=False, update_interval=0.0
|
||||
)
|
||||
|
||||
display.clear()
|
||||
|
||||
assert flushed == ["stream"]
|
||||
|
||||
|
||||
class TestEncodingFallback:
|
||||
def test_falls_back_when_the_stream_cannot_encode(self, display_cls):
|
||||
"""A cp1252-style console must not raise on emoji; it degrades instead."""
|
||||
|
||||
class AsciiOnly(io.StringIO):
|
||||
encoding = "ascii"
|
||||
|
||||
def write(self, text):
|
||||
text.encode("ascii") # raises UnicodeEncodeError on emoji
|
||||
return super().write(text)
|
||||
|
||||
buffer = AsciiOnly()
|
||||
display = display_cls(stream=buffer, use_emoji=True, update_interval=0.0)
|
||||
|
||||
display._safe_write("progress \U0001f504 bar\n")
|
||||
|
||||
assert "progress" in buffer.getvalue()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import MagicMock
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.vector_store.faiss_store import FAISSIndex
|
||||
from semantica.vector_store.faiss_store import FAISSIndex, FAISSStore
|
||||
|
||||
|
||||
def test_get_vector_reconstructs_from_flat_l2_index():
|
||||
@@ -120,3 +120,90 @@ def test_get_vector_reconstructs_from_real_ivfflat_index_without_prior_direct_ma
|
||||
result = index.get_vector("vec_target")
|
||||
|
||||
np.testing.assert_allclose(result, vectors[3], atol=1e-6)
|
||||
|
||||
|
||||
def _store_with_fake_index(ids, metadata_by_id=None):
|
||||
backend_index = MagicMock()
|
||||
backend_index.reconstruct.side_effect = lambda idx: [float(idx)] * 3
|
||||
index = FAISSIndex(backend_index, dimension=3)
|
||||
index.vector_ids = list(ids)
|
||||
index.metadata = dict(metadata_by_id or {})
|
||||
|
||||
store = FAISSStore(dimension=3)
|
||||
store.index = index
|
||||
return store
|
||||
|
||||
|
||||
def test_scan_vectors_returns_all_across_pages():
|
||||
store = _store_with_fake_index(["a", "b", "c", "d", "e"])
|
||||
|
||||
seen_ids = []
|
||||
offset = 0
|
||||
while True:
|
||||
page = store.scan_vectors(offset=offset, limit=2)
|
||||
if not page:
|
||||
break
|
||||
seen_ids.extend(p["id"] for p in page)
|
||||
offset += len(page)
|
||||
|
||||
assert seen_ids == ["a", "b", "c", "d", "e"]
|
||||
|
||||
|
||||
def test_scan_vectors_includes_vector_and_metadata():
|
||||
store = _store_with_fake_index(["a"], {"a": {"tag": "only"}})
|
||||
|
||||
page = store.scan_vectors(offset=0, limit=10)
|
||||
|
||||
assert len(page) == 1
|
||||
assert page[0]["id"] == "a"
|
||||
assert page[0]["metadata"] == {"tag": "only"}
|
||||
np.testing.assert_array_equal(page[0]["vector"], np.array([0.0, 0.0, 0.0], dtype=np.float32))
|
||||
|
||||
|
||||
def test_scan_vectors_no_index_returns_empty_list():
|
||||
store = FAISSStore(dimension=3)
|
||||
assert store.scan_vectors(offset=0, limit=10) == []
|
||||
|
||||
|
||||
def test_scan_vectors_zero_limit_returns_empty_list():
|
||||
store = _store_with_fake_index(["a"])
|
||||
assert store.scan_vectors(offset=0, limit=0) == []
|
||||
|
||||
|
||||
def test_scan_vectors_offset_past_end_returns_empty_list():
|
||||
store = _store_with_fake_index(["a"])
|
||||
assert store.scan_vectors(offset=100, limit=10) == []
|
||||
|
||||
|
||||
def test_add_vectors_retry_with_same_ids_does_not_duplicate():
|
||||
"""Re-running add_vectors with ids already in the index (e.g. retrying
|
||||
an interrupted migration) must not create a second physical vector
|
||||
under the same id."""
|
||||
backend_index = MagicMock()
|
||||
store = FAISSStore(dimension=3)
|
||||
store.index = FAISSIndex(backend_index, dimension=3)
|
||||
|
||||
vectors = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], dtype=np.float32)
|
||||
ids = ["a", "b", "c", "d"]
|
||||
|
||||
store.add_vectors(vectors, ids=ids, metadata=[{"i": i} for i in range(4)])
|
||||
assert store.count() == 4
|
||||
|
||||
store.add_vectors(vectors, ids=ids, metadata=[{"i": i} for i in range(4)])
|
||||
|
||||
assert store.count() == 4
|
||||
assert store.index.vector_ids == ids
|
||||
|
||||
|
||||
def test_add_vectors_retry_with_partial_overlap_only_adds_new_ids():
|
||||
backend_index = MagicMock()
|
||||
store = FAISSStore(dimension=3)
|
||||
store.index = FAISSIndex(backend_index, dimension=3)
|
||||
|
||||
store.add_vectors(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32), ids=["a", "b"])
|
||||
store.add_vectors(np.array([[1, 2, 3], [7, 8, 9]], dtype=np.float32), ids=["a", "c"])
|
||||
|
||||
assert store.index.vector_ids == ["a", "b", "c"]
|
||||
second_call_vectors = backend_index.add.call_args[0][0]
|
||||
assert second_call_vectors.shape[0] == 1
|
||||
np.testing.assert_array_equal(second_call_vectors[0], np.array([7, 8, 9], dtype=np.float32))
|
||||
|
||||
@@ -467,6 +467,48 @@ class TestPgVectorStoreDelete:
|
||||
assert success is True
|
||||
|
||||
|
||||
class TestPgVectorStoreScan:
|
||||
"""Test scan_vectors pagination."""
|
||||
|
||||
def test_scan_returns_all_vectors_across_pages(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32) for _ in range(5)]
|
||||
ids = store.add(vectors, [{"index": i} for i in range(5)])
|
||||
|
||||
seen_ids = []
|
||||
offset = 0
|
||||
while True:
|
||||
page = store.scan_vectors(offset=offset, limit=2)
|
||||
if not page:
|
||||
break
|
||||
seen_ids.extend(p["id"] for p in page)
|
||||
offset += len(page)
|
||||
|
||||
assert set(seen_ids) == set(ids)
|
||||
assert len(seen_ids) == 5
|
||||
|
||||
def test_scan_page_includes_vector_and_metadata(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32)]
|
||||
ids = store.add(vectors, [{"tag": "only"}])
|
||||
|
||||
page = store.scan_vectors(offset=0, limit=10)
|
||||
|
||||
assert len(page) == 1
|
||||
assert page[0]["id"] == ids[0]
|
||||
assert page[0]["metadata"] == {"tag": "only"}
|
||||
assert page[0]["vector"] is not None
|
||||
|
||||
def test_scan_empty_store_returns_empty_list(self, store):
|
||||
assert store.scan_vectors(offset=0, limit=10) == []
|
||||
|
||||
def test_scan_zero_limit_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=0, limit=0) == []
|
||||
|
||||
def test_scan_offset_past_end_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=100, limit=10) == []
|
||||
|
||||
|
||||
class TestPgVectorStoreIndex:
|
||||
"""Test index creation operations."""
|
||||
|
||||
|
||||
@@ -415,6 +415,48 @@ class TestSQLiteVecStoreStats:
|
||||
assert stats["vector_count"] == 4
|
||||
|
||||
|
||||
class TestSQLiteVecStoreScan:
|
||||
"""Test scan_vectors pagination."""
|
||||
|
||||
def test_scan_returns_all_vectors_across_pages(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32) for _ in range(5)]
|
||||
ids = store.add(vectors, [{"index": i} for i in range(5)])
|
||||
|
||||
seen_ids = []
|
||||
offset = 0
|
||||
while True:
|
||||
page = store.scan_vectors(offset=offset, limit=2)
|
||||
if not page:
|
||||
break
|
||||
seen_ids.extend(p["id"] for p in page)
|
||||
offset += len(page)
|
||||
|
||||
assert set(seen_ids) == set(ids)
|
||||
assert len(seen_ids) == 5
|
||||
|
||||
def test_scan_page_includes_vector_and_metadata(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32)]
|
||||
ids = store.add(vectors, [{"tag": "only"}])
|
||||
|
||||
page = store.scan_vectors(offset=0, limit=10)
|
||||
|
||||
assert len(page) == 1
|
||||
assert page[0]["id"] == ids[0]
|
||||
assert page[0]["metadata"] == {"tag": "only"}
|
||||
assert page[0]["vector"] is not None
|
||||
|
||||
def test_scan_empty_store_returns_empty_list(self, store):
|
||||
assert store.scan_vectors(offset=0, limit=10) == []
|
||||
|
||||
def test_scan_zero_limit_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=0, limit=0) == []
|
||||
|
||||
def test_scan_offset_past_end_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=100, limit=10) == []
|
||||
|
||||
|
||||
class TestSQLiteVecStoreFilterByMetadata:
|
||||
"""Test filter_by_metadata, including list-valued metadata handling."""
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user