yanantin 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yanantin-0.1.0/.gitignore +15 -0
- yanantin-0.1.0/PKG-INFO +18 -0
- yanantin-0.1.0/README.md +0 -0
- yanantin-0.1.0/conftest.py +27 -0
- yanantin-0.1.0/pyproject.toml +47 -0
- yanantin-0.1.0/src/yanantin/__init__.py +3 -0
- yanantin-0.1.0/src/yanantin/activity/__init__.py +30 -0
- yanantin-0.1.0/src/yanantin/activity/anchor.py +190 -0
- yanantin-0.1.0/src/yanantin/activity/backends/__init__.py +18 -0
- yanantin-0.1.0/src/yanantin/activity/backends/arango.py +308 -0
- yanantin-0.1.0/src/yanantin/activity/backends/duckdb.py +255 -0
- yanantin-0.1.0/src/yanantin/activity/backends/memory.py +154 -0
- yanantin-0.1.0/src/yanantin/activity/models.py +122 -0
- yanantin-0.1.0/src/yanantin/activity/store.py +90 -0
- yanantin-0.1.0/src/yanantin/apacheta/__init__.py +42 -0
- yanantin-0.1.0/src/yanantin/apacheta/backends/__init__.py +1 -0
- yanantin-0.1.0/src/yanantin/apacheta/backends/arango.py +739 -0
- yanantin-0.1.0/src/yanantin/apacheta/backends/duckdb.py +570 -0
- yanantin-0.1.0/src/yanantin/apacheta/backends/memory.py +545 -0
- yanantin-0.1.0/src/yanantin/apacheta/clients/__init__.py +5 -0
- yanantin-0.1.0/src/yanantin/apacheta/clients/gateway.py +399 -0
- yanantin-0.1.0/src/yanantin/apacheta/clients/openrouter.py +177 -0
- yanantin-0.1.0/src/yanantin/apacheta/config.py +241 -0
- yanantin-0.1.0/src/yanantin/apacheta/content_address.py +248 -0
- yanantin-0.1.0/src/yanantin/apacheta/ingest/__init__.py +1 -0
- yanantin-0.1.0/src/yanantin/apacheta/ingest/markdown_parser.py +408 -0
- yanantin-0.1.0/src/yanantin/apacheta/ingest/tensor_ballot.py +86 -0
- yanantin-0.1.0/src/yanantin/apacheta/interface/__init__.py +19 -0
- yanantin-0.1.0/src/yanantin/apacheta/interface/abstract.py +293 -0
- yanantin-0.1.0/src/yanantin/apacheta/interface/errors.py +21 -0
- yanantin-0.1.0/src/yanantin/apacheta/models/__init__.py +51 -0
- yanantin-0.1.0/src/yanantin/apacheta/models/base.py +20 -0
- yanantin-0.1.0/src/yanantin/apacheta/models/composition.py +97 -0
- yanantin-0.1.0/src/yanantin/apacheta/models/entities.py +26 -0
- yanantin-0.1.0/src/yanantin/apacheta/models/epistemics.py +65 -0
- yanantin-0.1.0/src/yanantin/apacheta/models/provenance.py +33 -0
- yanantin-0.1.0/src/yanantin/apacheta/models/tensor.py +66 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/__init__.py +1 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/bootstrap.py +48 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/compose.py +41 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/correct.py +53 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/dissent.py +48 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/evolve.py +30 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/negate.py +45 -0
- yanantin-0.1.0/src/yanantin/apacheta/operators/project.py +34 -0
- yanantin-0.1.0/src/yanantin/apacheta/renderer/__init__.py +1 -0
- yanantin-0.1.0/src/yanantin/apacheta/renderer/markdown.py +155 -0
- yanantin-0.1.0/src/yanantin/apacheta/rummage.py +352 -0
- yanantin-0.1.0/src/yanantin/apacheta/storage_obfuscator.py +70 -0
- yanantin-0.1.0/src/yanantin/awaq/__init__.py +10 -0
- yanantin-0.1.0/src/yanantin/awaq/__main__.py +140 -0
- yanantin-0.1.0/src/yanantin/awaq/materialize.py +315 -0
- yanantin-0.1.0/src/yanantin/awaq/weaver.py +855 -0
- yanantin-0.1.0/src/yanantin/chasqui/__init__.py +1 -0
- yanantin-0.1.0/src/yanantin/chasqui/__main__.py +270 -0
- yanantin-0.1.0/src/yanantin/chasqui/analyst.py +628 -0
- yanantin-0.1.0/src/yanantin/chasqui/attestation.py +244 -0
- yanantin-0.1.0/src/yanantin/chasqui/coordinator.py +1008 -0
- yanantin-0.1.0/src/yanantin/chasqui/coverage.py +449 -0
- yanantin-0.1.0/src/yanantin/chasqui/gleaner.py +801 -0
- yanantin-0.1.0/src/yanantin/chasqui/model_selector.py +124 -0
- yanantin-0.1.0/src/yanantin/chasqui/scorer.py +585 -0
- yanantin-0.1.0/src/yanantin/chasqui/scourer.py +537 -0
- yanantin-0.1.0/src/yanantin/chasqui/scout.py +610 -0
- yanantin-0.1.0/src/yanantin/collector/__init__.py +107 -0
- yanantin-0.1.0/src/yanantin/collector/__main__.py +558 -0
- yanantin-0.1.0/src/yanantin/collector/base.py +215 -0
- yanantin-0.1.0/src/yanantin/collector/checksum.py +347 -0
- yanantin-0.1.0/src/yanantin/collector/dropbox/__init__.py +24 -0
- yanantin-0.1.0/src/yanantin/collector/dropbox/collector.py +262 -0
- yanantin-0.1.0/src/yanantin/collector/dropbox/fact_recorder.py +70 -0
- yanantin-0.1.0/src/yanantin/collector/dropbox/models.py +103 -0
- yanantin-0.1.0/src/yanantin/collector/dropbox/recorder.py +118 -0
- yanantin-0.1.0/src/yanantin/collector/dropbox/synthetic.py +160 -0
- yanantin-0.1.0/src/yanantin/collector/filesystem/__init__.py +29 -0
- yanantin-0.1.0/src/yanantin/collector/filesystem/collector.py +182 -0
- yanantin-0.1.0/src/yanantin/collector/filesystem/fact_recorder.py +66 -0
- yanantin-0.1.0/src/yanantin/collector/filesystem/models.py +134 -0
- yanantin-0.1.0/src/yanantin/collector/filesystem/recorder.py +116 -0
- yanantin-0.1.0/src/yanantin/collector/filesystem/synthetic.py +212 -0
- yanantin-0.1.0/src/yanantin/collector/fs_events/__init__.py +23 -0
- yanantin-0.1.0/src/yanantin/collector/fs_events/collector.py +190 -0
- yanantin-0.1.0/src/yanantin/collector/fs_events/fact_recorder.py +66 -0
- yanantin-0.1.0/src/yanantin/collector/fs_events/models.py +69 -0
- yanantin-0.1.0/src/yanantin/collector/fs_events/recorder.py +116 -0
- yanantin-0.1.0/src/yanantin/collector/fs_events/synthetic.py +116 -0
- yanantin-0.1.0/src/yanantin/collector/machine_config.py +259 -0
- yanantin-0.1.0/src/yanantin/collector/models.py +66 -0
- yanantin-0.1.0/src/yanantin/collector/openrouter/__init__.py +15 -0
- yanantin-0.1.0/src/yanantin/collector/openrouter/collector.py +139 -0
- yanantin-0.1.0/src/yanantin/collector/openrouter/fact_recorder.py +74 -0
- yanantin-0.1.0/src/yanantin/collector/openrouter/models.py +75 -0
- yanantin-0.1.0/src/yanantin/collector/pipeline.py +114 -0
- yanantin-0.1.0/src/yanantin/collector/synthetic.py +57 -0
- yanantin-0.1.0/src/yanantin/collector/wranglers.py +144 -0
- yanantin-0.1.0/src/yanantin/infra/__init__.py +15 -0
- yanantin-0.1.0/src/yanantin/infra/__main__.py +147 -0
- yanantin-0.1.0/src/yanantin/infra/config.py +211 -0
- yanantin-0.1.0/src/yanantin/infra/docker.py +191 -0
- yanantin-0.1.0/src/yanantin/infra/orchestrator.py +181 -0
- yanantin-0.1.0/src/yanantin/jabberwock/__init__.py +49 -0
- yanantin-0.1.0/src/yanantin/jabberwock/__main__.py +309 -0
- yanantin-0.1.0/src/yanantin/jabberwock/brillig.py +453 -0
- yanantin-0.1.0/src/yanantin/jabberwock/models.py +247 -0
- yanantin-0.1.0/src/yanantin/jabberwock/normalize.py +66 -0
- yanantin-0.1.0/src/yanantin/provenance/__init__.py +48 -0
- yanantin-0.1.0/src/yanantin/provenance/timestamp.py +486 -0
- yanantin-0.1.0/src/yanantin/query/__init__.py +21 -0
- yanantin-0.1.0/src/yanantin/query/__main__.py +244 -0
- yanantin-0.1.0/src/yanantin/query/engine.py +196 -0
- yanantin-0.1.0/src/yanantin/query/models.py +91 -0
- yanantin-0.1.0/src/yanantin/query/recorder.py +54 -0
- yanantin-0.1.0/src/yanantin/tinkuy/__init__.py +5 -0
- yanantin-0.1.0/src/yanantin/tinkuy/__main__.py +67 -0
- yanantin-0.1.0/src/yanantin/tinkuy/audit.py +275 -0
- yanantin-0.1.0/src/yanantin/tinkuy/succession.py +204 -0
- yanantin-0.1.0/tests/__init__.py +0 -0
- yanantin-0.1.0/tests/integration/__init__.py +10 -0
- yanantin-0.1.0/tests/integration/test_arango_activity.py +561 -0
- yanantin-0.1.0/tests/integration/test_arango_real.py +1291 -0
- yanantin-0.1.0/tests/integration/test_open_record_queries_arango_real.py +183 -0
- yanantin-0.1.0/tests/red_bar/__init__.py +0 -0
- yanantin-0.1.0/tests/red_bar/test_activity_stream.py +488 -0
- yanantin-0.1.0/tests/red_bar/test_attestation_invariants.py +246 -0
- yanantin-0.1.0/tests/red_bar/test_governance.py +369 -0
- yanantin-0.1.0/tests/red_bar/test_immutability.py +58 -0
- yanantin-0.1.0/tests/red_bar/test_jabberwock_cli_invariants.py +126 -0
- yanantin-0.1.0/tests/red_bar/test_jabberwock_invariants.py +250 -0
- yanantin-0.1.0/tests/red_bar/test_least_privilege.py +206 -0
- yanantin-0.1.0/tests/red_bar/test_monotonicity.py +73 -0
- yanantin-0.1.0/tests/red_bar/test_portability.py +80 -0
- yanantin-0.1.0/tests/red_bar/test_provenance.py +100 -0
- yanantin-0.1.0/tests/red_bar/test_query_pipeline.py +107 -0
- yanantin-0.1.0/tests/unit/__init__.py +0 -0
- yanantin-0.1.0/tests/unit/test_activity_store.py +341 -0
- yanantin-0.1.0/tests/unit/test_analyst.py +555 -0
- yanantin-0.1.0/tests/unit/test_arango_independent.py +1280 -0
- yanantin-0.1.0/tests/unit/test_attestation.py +311 -0
- yanantin-0.1.0/tests/unit/test_awaq_weaver.py +627 -0
- yanantin-0.1.0/tests/unit/test_chasqui.py +110 -0
- yanantin-0.1.0/tests/unit/test_collector.py +200 -0
- yanantin-0.1.0/tests/unit/test_collector_checksum.py +163 -0
- yanantin-0.1.0/tests/unit/test_collector_cli.py +83 -0
- yanantin-0.1.0/tests/unit/test_collector_dropbox.py +124 -0
- yanantin-0.1.0/tests/unit/test_collector_filesystem.py +227 -0
- yanantin-0.1.0/tests/unit/test_collector_fs_events.py +208 -0
- yanantin-0.1.0/tests/unit/test_collector_isomorphism.py +422 -0
- yanantin-0.1.0/tests/unit/test_collector_synthetic.py +86 -0
- yanantin-0.1.0/tests/unit/test_config_tensors.py +242 -0
- yanantin-0.1.0/tests/unit/test_content_address.py +336 -0
- yanantin-0.1.0/tests/unit/test_coverage.py +427 -0
- yanantin-0.1.0/tests/unit/test_duckdb_backend.py +602 -0
- yanantin-0.1.0/tests/unit/test_duckdb_independent.py +1874 -0
- yanantin-0.1.0/tests/unit/test_fact_recorders.py +240 -0
- yanantin-0.1.0/tests/unit/test_gateway_client_independent.py +1158 -0
- yanantin-0.1.0/tests/unit/test_gleaner.py +1163 -0
- yanantin-0.1.0/tests/unit/test_infra_config.py +142 -0
- yanantin-0.1.0/tests/unit/test_infra_docker.py +121 -0
- yanantin-0.1.0/tests/unit/test_infra_orchestrator.py +90 -0
- yanantin-0.1.0/tests/unit/test_ingest.py +90 -0
- yanantin-0.1.0/tests/unit/test_interface.py +44 -0
- yanantin-0.1.0/tests/unit/test_jabberwock_brillig.py +768 -0
- yanantin-0.1.0/tests/unit/test_jabberwock_cli.py +462 -0
- yanantin-0.1.0/tests/unit/test_jabberwock_models.py +596 -0
- yanantin-0.1.0/tests/unit/test_jabberwock_normalize.py +110 -0
- yanantin-0.1.0/tests/unit/test_machine_config.py +136 -0
- yanantin-0.1.0/tests/unit/test_materialize.py +327 -0
- yanantin-0.1.0/tests/unit/test_memory_anchor.py +248 -0
- yanantin-0.1.0/tests/unit/test_memory_backend.py +587 -0
- yanantin-0.1.0/tests/unit/test_models.py +509 -0
- yanantin-0.1.0/tests/unit/test_open_record_queries_memory.py +80 -0
- yanantin-0.1.0/tests/unit/test_open_record_queries_not_implemented.py +44 -0
- yanantin-0.1.0/tests/unit/test_openrouter.py +44 -0
- yanantin-0.1.0/tests/unit/test_operators.py +208 -0
- yanantin-0.1.0/tests/unit/test_precompact_hook.py +1607 -0
- yanantin-0.1.0/tests/unit/test_provenance_timestamp.py +1052 -0
- yanantin-0.1.0/tests/unit/test_query_engine.py +445 -0
- yanantin-0.1.0/tests/unit/test_query_models.py +137 -0
- yanantin-0.1.0/tests/unit/test_recorders.py +509 -0
- yanantin-0.1.0/tests/unit/test_renderer.py +182 -0
- yanantin-0.1.0/tests/unit/test_scorer.py +153 -0
- yanantin-0.1.0/tests/unit/test_scourer.py +545 -0
- yanantin-0.1.0/tests/unit/test_scout_features.py +269 -0
- yanantin-0.1.0/tests/unit/test_tinkuy_audit.py +191 -0
- yanantin-0.1.0/tests/unit/test_tinkuy_succession.py +199 -0
yanantin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yanantin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Complementary duality between human and AI — composable tensor infrastructure
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: anthropic>=0.84.0
|
|
7
|
+
Requires-Dist: docker>=7.1.0
|
|
8
|
+
Requires-Dist: duckdb>=1.4.4
|
|
9
|
+
Requires-Dist: flask>=3.1.3
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: numpy>=2.4.2
|
|
12
|
+
Requires-Dist: opentimestamps>=0.4.5
|
|
13
|
+
Requires-Dist: pydantic>=2.0
|
|
14
|
+
Requires-Dist: python-arango>=8.2.6
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
17
|
+
Provides-Extra: dropbox
|
|
18
|
+
Requires-Dist: dropbox>=12.0; extra == 'dropbox'
|
yanantin-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Root conftest: loads .env into os.environ at test startup.
|
|
2
|
+
|
|
3
|
+
Uses stdlib only. Environment variables already set take precedence
|
|
4
|
+
(setdefault, not overwrite). This replaces hardcoded credentials
|
|
5
|
+
in integration test files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pytest_configure(config) -> None:
|
|
15
|
+
"""Load .env file if it exists."""
|
|
16
|
+
env_file = Path(__file__).parent / ".env"
|
|
17
|
+
if not env_file.exists():
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
for line in env_file.read_text().splitlines():
|
|
21
|
+
line = line.strip()
|
|
22
|
+
if not line or line.startswith("#"):
|
|
23
|
+
continue
|
|
24
|
+
if "=" not in line:
|
|
25
|
+
continue
|
|
26
|
+
key, _, value = line.partition("=")
|
|
27
|
+
os.environ.setdefault(key.strip(), value.strip())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "yanantin"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Complementary duality between human and AI — composable tensor infrastructure"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"opentimestamps>=0.4.5",
|
|
9
|
+
"pydantic>=2.0",
|
|
10
|
+
"httpx>=0.27",
|
|
11
|
+
"duckdb>=1.4.4",
|
|
12
|
+
"python-arango>=8.2.6",
|
|
13
|
+
"flask>=3.1.3",
|
|
14
|
+
"numpy>=2.4.2",
|
|
15
|
+
"anthropic>=0.84.0",
|
|
16
|
+
"docker>=7.1.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
]
|
|
23
|
+
dropbox = [
|
|
24
|
+
"dropbox>=12.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.metadata]
|
|
35
|
+
allow-direct-references = true
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/yanantin"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = [
|
|
42
|
+
"src/yanantin",
|
|
43
|
+
"tests",
|
|
44
|
+
"conftest.py",
|
|
45
|
+
"pyproject.toml",
|
|
46
|
+
"README.md",
|
|
47
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Activity stream layer — temporal fact storage and memory anchors.
|
|
2
|
+
|
|
3
|
+
Two stores, two concerns:
|
|
4
|
+
- **ActivityStreamStore** — temporal fact storage. High volume, append-only,
|
|
5
|
+
indexed by (provider_id, timestamp). Schema-agnostic.
|
|
6
|
+
- **ApachetaInterface** — authored tensor storage. Low volume, immutable,
|
|
7
|
+
queryable by epistemic metadata. Existing, unchanged.
|
|
8
|
+
|
|
9
|
+
The **MemoryAnchorService** bridges them: it tracks which providers have
|
|
10
|
+
new data (cursors), issues handles to callers, and only persists when
|
|
11
|
+
the write gate opens (updated AND referenced).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from yanantin.activity.anchor import MemoryAnchorService
|
|
15
|
+
from yanantin.activity.models import (
|
|
16
|
+
AnchorCursor,
|
|
17
|
+
AnchorView,
|
|
18
|
+
FactRecord,
|
|
19
|
+
MemoryAnchor,
|
|
20
|
+
)
|
|
21
|
+
from yanantin.activity.store import ActivityStreamStore
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ActivityStreamStore",
|
|
25
|
+
"AnchorCursor",
|
|
26
|
+
"AnchorView",
|
|
27
|
+
"FactRecord",
|
|
28
|
+
"MemoryAnchor",
|
|
29
|
+
"MemoryAnchorService",
|
|
30
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Memory anchor service — the bridge between facts and tensors.
|
|
2
|
+
|
|
3
|
+
Issues handles, tracks cursors, manages the two-flag write gate
|
|
4
|
+
(updated AND referenced). Implements Indaleko's ActivityContextService
|
|
5
|
+
pattern: write only when something changed AND someone asked for it.
|
|
6
|
+
|
|
7
|
+
The service owns the lifecycle:
|
|
8
|
+
- update_cursor() — provider reports new data (sets updated)
|
|
9
|
+
- get_handle() — caller requests current position (sets referenced)
|
|
10
|
+
- flush() — write gate check, persist if both flags set
|
|
11
|
+
- materialize() — resolve an anchor against current streams (always fresh)
|
|
12
|
+
- freeze() — pin a temporal view into a permanent tensor (authored act)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from uuid import UUID, uuid4
|
|
20
|
+
|
|
21
|
+
from yanantin.activity.models import AnchorCursor, AnchorView, FactRecord, MemoryAnchor
|
|
22
|
+
from yanantin.activity.store import ActivityStreamStore
|
|
23
|
+
from yanantin.apacheta.interface import ApachetaInterface
|
|
24
|
+
from yanantin.apacheta.models import (
|
|
25
|
+
ProvenanceEnvelope,
|
|
26
|
+
SourceIdentifier,
|
|
27
|
+
StrandRecord,
|
|
28
|
+
TensorRecord,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MemoryAnchorService:
|
|
33
|
+
"""Issues handles, tracks cursors, manages the write gate.
|
|
34
|
+
|
|
35
|
+
The service is the bridge between the fact store and the tensor store.
|
|
36
|
+
It implements Indaleko's two-flag write gate: updated AND referenced.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, store: ActivityStreamStore) -> None:
|
|
40
|
+
self._store = store
|
|
41
|
+
self._handle: UUID = uuid4()
|
|
42
|
+
self._timestamp: datetime = datetime.now(timezone.utc)
|
|
43
|
+
self._cursors: dict[UUID, AnchorCursor] = {}
|
|
44
|
+
self._updated: bool = False
|
|
45
|
+
self._referenced: bool = False
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def store(self) -> ActivityStreamStore:
|
|
49
|
+
"""The underlying activity stream store."""
|
|
50
|
+
return self._store
|
|
51
|
+
|
|
52
|
+
def update_cursor(
|
|
53
|
+
self,
|
|
54
|
+
provider: UUID,
|
|
55
|
+
reference: UUID,
|
|
56
|
+
data: str | None = None,
|
|
57
|
+
attributes: dict[str, str] | None = None,
|
|
58
|
+
) -> bool:
|
|
59
|
+
"""Update a provider's cursor position.
|
|
60
|
+
|
|
61
|
+
If the provider exists and reference is unchanged, returns False
|
|
62
|
+
(no-op). Otherwise updates/adds the cursor and sets updated=True.
|
|
63
|
+
"""
|
|
64
|
+
existing = self._cursors.get(provider)
|
|
65
|
+
if existing is not None and existing.reference == reference:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
self._cursors[provider] = AnchorCursor(
|
|
69
|
+
provider=provider,
|
|
70
|
+
reference=reference,
|
|
71
|
+
data=data,
|
|
72
|
+
attributes=attributes,
|
|
73
|
+
)
|
|
74
|
+
self._updated = True
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
def get_handle(self) -> UUID:
|
|
78
|
+
"""Get the current anchor handle, setting referenced=True."""
|
|
79
|
+
self._referenced = True
|
|
80
|
+
return self._handle
|
|
81
|
+
|
|
82
|
+
def flush(self) -> bool:
|
|
83
|
+
"""Persist anchor if both flags are set (updated AND referenced).
|
|
84
|
+
|
|
85
|
+
If the write gate is closed, returns False without writing.
|
|
86
|
+
If both flags are set: builds MemoryAnchor, stores it, advances
|
|
87
|
+
handle and timestamp, resets flags. Returns True.
|
|
88
|
+
"""
|
|
89
|
+
if not (self._updated and self._referenced):
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
anchor = MemoryAnchor(
|
|
93
|
+
handle=self._handle,
|
|
94
|
+
timestamp=self._timestamp,
|
|
95
|
+
cursors=tuple(self._cursors.values()),
|
|
96
|
+
)
|
|
97
|
+
self._store.store_anchor(anchor)
|
|
98
|
+
|
|
99
|
+
# Advance: new handle, new timestamp, reset flags
|
|
100
|
+
self._handle = uuid4()
|
|
101
|
+
self._timestamp = datetime.now(timezone.utc)
|
|
102
|
+
self._updated = False
|
|
103
|
+
self._referenced = False
|
|
104
|
+
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def materialize(self, handle: UUID) -> AnchorView:
|
|
108
|
+
"""Resolve an anchor against current streams.
|
|
109
|
+
|
|
110
|
+
Always fresh — queries list_providers() at resolution time
|
|
111
|
+
(late binding), not just the providers in the anchor's cursor
|
|
112
|
+
list. A new provider registered after the anchor was created
|
|
113
|
+
will appear in the view if it has facts before the anchor's
|
|
114
|
+
timestamp.
|
|
115
|
+
"""
|
|
116
|
+
anchor = self._store.get_anchor(handle)
|
|
117
|
+
|
|
118
|
+
# Late binding: discover ALL current providers
|
|
119
|
+
all_providers = self._store.list_providers()
|
|
120
|
+
|
|
121
|
+
# For each provider, find the latest fact at or before anchor time
|
|
122
|
+
facts: dict[UUID, FactRecord] = {}
|
|
123
|
+
for provider_id in all_providers:
|
|
124
|
+
fact = self._store.query_latest(provider_id, before=anchor.timestamp)
|
|
125
|
+
if fact is not None:
|
|
126
|
+
facts[provider_id] = fact
|
|
127
|
+
|
|
128
|
+
return AnchorView(
|
|
129
|
+
handle=anchor.handle,
|
|
130
|
+
timestamp=anchor.timestamp,
|
|
131
|
+
facts=facts,
|
|
132
|
+
providers=tuple(all_providers),
|
|
133
|
+
anchor=anchor,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def freeze(self, handle: UUID, interface: ApachetaInterface) -> UUID:
|
|
137
|
+
"""Pin a temporal view into a permanent tensor.
|
|
138
|
+
|
|
139
|
+
This is an authored act — it decides to freeze a temporal view.
|
|
140
|
+
The resulting tensor has provenance and structured content.
|
|
141
|
+
"""
|
|
142
|
+
view = self.materialize(handle)
|
|
143
|
+
|
|
144
|
+
# Strand 0: Anchor summary
|
|
145
|
+
summary_lines = [
|
|
146
|
+
f"handle: {view.handle}",
|
|
147
|
+
f"timestamp: {view.timestamp.isoformat()}",
|
|
148
|
+
f"provider_count: {len(view.providers)}",
|
|
149
|
+
f"fact_count: {len(view.facts)}",
|
|
150
|
+
]
|
|
151
|
+
summary_strand = StrandRecord(
|
|
152
|
+
strand_index=0,
|
|
153
|
+
title="Anchor Summary",
|
|
154
|
+
content="\n".join(summary_lines),
|
|
155
|
+
topics=("anchor", "summary"),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Strand 1: Anchor data — cursor state + resolved facts
|
|
159
|
+
anchor_data = {
|
|
160
|
+
"cursors": [c.model_dump(mode="json") for c in view.anchor.cursors],
|
|
161
|
+
"resolved_facts": {
|
|
162
|
+
str(pid): f.model_dump(mode="json")
|
|
163
|
+
for pid, f in view.facts.items()
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
data_strand = StrandRecord(
|
|
167
|
+
strand_index=1,
|
|
168
|
+
title="Anchor Data",
|
|
169
|
+
content=json.dumps(anchor_data, sort_keys=True, separators=(",", ":")),
|
|
170
|
+
topics=("anchor", "data", "facts"),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
tensor = TensorRecord(
|
|
174
|
+
provenance=ProvenanceEnvelope(
|
|
175
|
+
source=SourceIdentifier(
|
|
176
|
+
identifier=view.handle,
|
|
177
|
+
description="Memory anchor freeze",
|
|
178
|
+
),
|
|
179
|
+
author_model_family="anchor-service",
|
|
180
|
+
),
|
|
181
|
+
preamble=(
|
|
182
|
+
f"Frozen anchor {view.handle} at {view.timestamp.isoformat()} "
|
|
183
|
+
f"({len(view.providers)} providers, {len(view.facts)} facts)"
|
|
184
|
+
),
|
|
185
|
+
strands=(summary_strand, data_strand),
|
|
186
|
+
lineage_tags=("anchor", "frozen-view"),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
interface.store_tensor(tensor)
|
|
190
|
+
return tensor.id
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Activity stream store backends.
|
|
2
|
+
|
|
3
|
+
Three backends, same contract:
|
|
4
|
+
- InMemoryActivityStreamStore — dict + bisect, for tests
|
|
5
|
+
- DuckDBActivityStreamStore — SQL query pushdown, file-backed
|
|
6
|
+
- ArangoDBActivityStreamStore — AQL + persistent sorted index, production
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from yanantin.activity.backends.memory import InMemoryActivityStreamStore
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"InMemoryActivityStreamStore",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# ArangoDB and DuckDB backends are not imported at package level to avoid
|
|
16
|
+
# requiring their dependencies (python-arango, duckdb) for all users.
|
|
17
|
+
# Import directly: from yanantin.activity.backends.arango import ArangoDBActivityStreamStore
|
|
18
|
+
# Import directly: from yanantin.activity.backends.duckdb import DuckDBActivityStreamStore
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""ArangoDB backend for the activity stream store.
|
|
2
|
+
|
|
3
|
+
Document-based persistent storage with index-driven temporal queries.
|
|
4
|
+
Uses AQL for query pushdown — the composite persistent index on
|
|
5
|
+
(provider_id, timestamp) makes temporal queries O(log n).
|
|
6
|
+
|
|
7
|
+
Design:
|
|
8
|
+
- Two collections: activity_facts and activity_anchors
|
|
9
|
+
- Persistent sorted index on activity_facts (provider_id, timestamp)
|
|
10
|
+
- Persistent sorted index on activity_anchors (timestamp)
|
|
11
|
+
- Thread safety via RLock
|
|
12
|
+
- Least-privilege: separate user with rw on these collections only
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import threading
|
|
18
|
+
from uuid import UUID
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
from arango import ArangoClient
|
|
22
|
+
from arango.database import StandardDatabase
|
|
23
|
+
|
|
24
|
+
from yanantin.activity.models import FactRecord, MemoryAnchor
|
|
25
|
+
from yanantin.activity.store import ActivityStreamStore
|
|
26
|
+
from yanantin.apacheta.interface.errors import ImmutabilityError, NotFoundError
|
|
27
|
+
from yanantin.apacheta.storage_obfuscator import StorageObfuscator, TransparentObfuscator
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_SEMANTIC_COLLECTIONS = ("activity_facts", "activity_anchors")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ArangoDBActivityStreamStore(ActivityStreamStore):
|
|
34
|
+
"""ArangoDB implementation of ActivityStreamStore.
|
|
35
|
+
|
|
36
|
+
Thread-safe via RLock. Enforces immutability. Uses AQL with
|
|
37
|
+
persistent sorted indexes for O(log n) temporal queries.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
host: str = "http://localhost:8529",
|
|
43
|
+
db_name: str = "apacheta",
|
|
44
|
+
username: str = "",
|
|
45
|
+
password: str = "",
|
|
46
|
+
obfuscator: StorageObfuscator | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._lock = threading.RLock()
|
|
49
|
+
self._map = obfuscator or TransparentObfuscator()
|
|
50
|
+
self._client = ArangoClient(hosts=host)
|
|
51
|
+
self._host = host
|
|
52
|
+
self._db_name = db_name
|
|
53
|
+
self._db = self._connect_database(username, password)
|
|
54
|
+
self._ensure_collections()
|
|
55
|
+
|
|
56
|
+
def _connect_database(self, username: str, password: str) -> StandardDatabase:
|
|
57
|
+
"""Connect to the target database. Fail-stop if it doesn't exist."""
|
|
58
|
+
try:
|
|
59
|
+
db = self._client.db(self._db_name, username=username, password=password)
|
|
60
|
+
db.collections()
|
|
61
|
+
return db
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise ConnectionError(
|
|
64
|
+
f"Cannot connect to ArangoDB database '{self._db_name}' at {self._host}. "
|
|
65
|
+
f"Database must be provisioned by an admin before the application can use it. "
|
|
66
|
+
f"Error: {e}"
|
|
67
|
+
) from e
|
|
68
|
+
|
|
69
|
+
def _ensure_collections(self) -> None:
|
|
70
|
+
"""Create collections and indexes if they don't exist."""
|
|
71
|
+
for name in _SEMANTIC_COLLECTIONS:
|
|
72
|
+
mapped = self._map.collection_name(name)
|
|
73
|
+
if not self._db.has_collection(mapped):
|
|
74
|
+
self._db.create_collection(mapped)
|
|
75
|
+
|
|
76
|
+
# Persistent sorted index for temporal queries on facts
|
|
77
|
+
facts_col = self._db.collection(self._map.collection_name("activity_facts"))
|
|
78
|
+
facts_col.add_index(
|
|
79
|
+
{
|
|
80
|
+
"type": "persistent",
|
|
81
|
+
"fields": [
|
|
82
|
+
self._map.field_name("provider_id"),
|
|
83
|
+
self._map.field_name("timestamp"),
|
|
84
|
+
],
|
|
85
|
+
"sparse": False,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Persistent sorted index for temporal anchor queries
|
|
90
|
+
anchors_col = self._db.collection(self._map.collection_name("activity_anchors"))
|
|
91
|
+
anchors_col.add_index(
|
|
92
|
+
{
|
|
93
|
+
"type": "persistent",
|
|
94
|
+
"fields": [self._map.field_name("timestamp")],
|
|
95
|
+
"sparse": False,
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def close(self) -> None:
|
|
100
|
+
self._client.close()
|
|
101
|
+
|
|
102
|
+
def __enter__(self):
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, *args):
|
|
106
|
+
self.close()
|
|
107
|
+
|
|
108
|
+
# -- Fact operations -----------------------------------------------
|
|
109
|
+
|
|
110
|
+
def store_fact(self, fact: FactRecord) -> None:
|
|
111
|
+
with self._lock:
|
|
112
|
+
col = self._db.collection(self._map.collection_name("activity_facts"))
|
|
113
|
+
key = str(fact.id)
|
|
114
|
+
if col.has(key):
|
|
115
|
+
raise ImmutabilityError(
|
|
116
|
+
f"Fact {fact.id} already exists. "
|
|
117
|
+
"Facts are immutable — append, don't overwrite."
|
|
118
|
+
)
|
|
119
|
+
doc = fact.model_dump(mode="json")
|
|
120
|
+
# Build mapped document: _key + mapped field names
|
|
121
|
+
mapped_doc = {"_key": key}
|
|
122
|
+
for k, v in doc.items():
|
|
123
|
+
if k == "id":
|
|
124
|
+
continue # moved to _key
|
|
125
|
+
elif k == "provider_id":
|
|
126
|
+
mapped_doc[self._map.field_name(k)] = str(v)
|
|
127
|
+
elif k == "timestamp":
|
|
128
|
+
mapped_doc[self._map.field_name(k)] = fact.timestamp.isoformat()
|
|
129
|
+
else:
|
|
130
|
+
mapped_doc[self._map.field_name(k)] = v
|
|
131
|
+
col.insert(mapped_doc)
|
|
132
|
+
|
|
133
|
+
def get_fact(self, fact_id: UUID) -> FactRecord:
|
|
134
|
+
with self._lock:
|
|
135
|
+
col = self._db.collection(self._map.collection_name("activity_facts"))
|
|
136
|
+
doc = col.get(str(fact_id))
|
|
137
|
+
if doc is None:
|
|
138
|
+
raise NotFoundError(f"Fact {fact_id} not found.")
|
|
139
|
+
return self._doc_to_fact(doc)
|
|
140
|
+
|
|
141
|
+
def query_latest(
|
|
142
|
+
self,
|
|
143
|
+
provider_id: UUID,
|
|
144
|
+
before: datetime | None = None,
|
|
145
|
+
) -> FactRecord | None:
|
|
146
|
+
with self._lock:
|
|
147
|
+
col = self._map.collection_name("activity_facts")
|
|
148
|
+
f_pid = self._map.field_name("provider_id")
|
|
149
|
+
f_ts = self._map.field_name("timestamp")
|
|
150
|
+
if before is not None:
|
|
151
|
+
cursor = self._db.aql.execute(
|
|
152
|
+
f"FOR doc IN {col} "
|
|
153
|
+
f" FILTER doc.{f_pid} == @provider_id "
|
|
154
|
+
f" FILTER doc.{f_ts} <= @before "
|
|
155
|
+
f" SORT doc.{f_ts} DESC "
|
|
156
|
+
f" LIMIT 1 "
|
|
157
|
+
f" RETURN doc",
|
|
158
|
+
bind_vars={
|
|
159
|
+
"provider_id": str(provider_id),
|
|
160
|
+
"before": before.isoformat(),
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
cursor = self._db.aql.execute(
|
|
165
|
+
f"FOR doc IN {col} "
|
|
166
|
+
f" FILTER doc.{f_pid} == @provider_id "
|
|
167
|
+
f" SORT doc.{f_ts} DESC "
|
|
168
|
+
f" LIMIT 1 "
|
|
169
|
+
f" RETURN doc",
|
|
170
|
+
bind_vars={"provider_id": str(provider_id)},
|
|
171
|
+
)
|
|
172
|
+
docs = list(cursor)
|
|
173
|
+
if not docs:
|
|
174
|
+
return None
|
|
175
|
+
return self._doc_to_fact(docs[0])
|
|
176
|
+
|
|
177
|
+
def query_range(
|
|
178
|
+
self,
|
|
179
|
+
provider_id: UUID,
|
|
180
|
+
start: datetime | None = None,
|
|
181
|
+
end: datetime | None = None,
|
|
182
|
+
) -> list[FactRecord]:
|
|
183
|
+
with self._lock:
|
|
184
|
+
col = self._map.collection_name("activity_facts")
|
|
185
|
+
f_pid = self._map.field_name("provider_id")
|
|
186
|
+
f_ts = self._map.field_name("timestamp")
|
|
187
|
+
|
|
188
|
+
filters = [f"doc.{f_pid} == @provider_id"]
|
|
189
|
+
bind_vars: dict = {"provider_id": str(provider_id)}
|
|
190
|
+
|
|
191
|
+
if start is not None:
|
|
192
|
+
filters.append(f"doc.{f_ts} >= @start")
|
|
193
|
+
bind_vars["start"] = start.isoformat()
|
|
194
|
+
if end is not None:
|
|
195
|
+
filters.append(f"doc.{f_ts} <= @end")
|
|
196
|
+
bind_vars["end"] = end.isoformat()
|
|
197
|
+
|
|
198
|
+
filter_clause = " FILTER ".join([""] + filters)
|
|
199
|
+
cursor = self._db.aql.execute(
|
|
200
|
+
f"FOR doc IN {col}"
|
|
201
|
+
f" {filter_clause}"
|
|
202
|
+
f" SORT doc.{f_ts} ASC"
|
|
203
|
+
f" RETURN doc",
|
|
204
|
+
bind_vars=bind_vars,
|
|
205
|
+
)
|
|
206
|
+
return [self._doc_to_fact(doc) for doc in cursor]
|
|
207
|
+
|
|
208
|
+
# -- Anchor operations ---------------------------------------------
|
|
209
|
+
|
|
210
|
+
def store_anchor(self, anchor: MemoryAnchor) -> None:
|
|
211
|
+
with self._lock:
|
|
212
|
+
col = self._db.collection(self._map.collection_name("activity_anchors"))
|
|
213
|
+
key = str(anchor.handle)
|
|
214
|
+
if col.has(key):
|
|
215
|
+
raise ImmutabilityError(
|
|
216
|
+
f"Anchor {anchor.handle} already exists. "
|
|
217
|
+
"Anchors are immutable — advance, don't overwrite."
|
|
218
|
+
)
|
|
219
|
+
doc = anchor.model_dump(mode="json")
|
|
220
|
+
# Build mapped document
|
|
221
|
+
mapped_doc = {"_key": key}
|
|
222
|
+
for k, v in doc.items():
|
|
223
|
+
if k == "handle":
|
|
224
|
+
continue # moved to _key
|
|
225
|
+
elif k == "timestamp":
|
|
226
|
+
mapped_doc[self._map.field_name(k)] = anchor.timestamp.isoformat()
|
|
227
|
+
elif k == "cursors":
|
|
228
|
+
# Cursors contain model fields --- obfuscate nested dicts
|
|
229
|
+
mapped_doc[self._map.field_name(k)] = [
|
|
230
|
+
self._map.obfuscate_document(c) if isinstance(c, dict) else c
|
|
231
|
+
for c in v
|
|
232
|
+
]
|
|
233
|
+
else:
|
|
234
|
+
mapped_doc[self._map.field_name(k)] = v
|
|
235
|
+
col.insert(mapped_doc)
|
|
236
|
+
|
|
237
|
+
def get_anchor(self, handle: UUID) -> MemoryAnchor:
|
|
238
|
+
with self._lock:
|
|
239
|
+
col = self._db.collection(self._map.collection_name("activity_anchors"))
|
|
240
|
+
doc = col.get(str(handle))
|
|
241
|
+
if doc is None:
|
|
242
|
+
raise NotFoundError(f"Anchor {handle} not found.")
|
|
243
|
+
return self._doc_to_anchor(doc)
|
|
244
|
+
|
|
245
|
+
def get_latest_anchor(self) -> MemoryAnchor | None:
|
|
246
|
+
with self._lock:
|
|
247
|
+
col = self._map.collection_name("activity_anchors")
|
|
248
|
+
f_ts = self._map.field_name("timestamp")
|
|
249
|
+
cursor = self._db.aql.execute(
|
|
250
|
+
f"FOR doc IN {col} "
|
|
251
|
+
f" SORT doc.{f_ts} DESC "
|
|
252
|
+
f" LIMIT 1 "
|
|
253
|
+
f" RETURN doc"
|
|
254
|
+
)
|
|
255
|
+
docs = list(cursor)
|
|
256
|
+
if not docs:
|
|
257
|
+
return None
|
|
258
|
+
return self._doc_to_anchor(docs[0])
|
|
259
|
+
|
|
260
|
+
# -- Discovery -----------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def list_providers(self) -> list[UUID]:
|
|
263
|
+
with self._lock:
|
|
264
|
+
col = self._map.collection_name("activity_facts")
|
|
265
|
+
f_pid = self._map.field_name("provider_id")
|
|
266
|
+
cursor = self._db.aql.execute(
|
|
267
|
+
f"FOR doc IN {col} "
|
|
268
|
+
f" COLLECT provider = doc.{f_pid} "
|
|
269
|
+
f" RETURN provider"
|
|
270
|
+
)
|
|
271
|
+
return [UUID(p) for p in cursor]
|
|
272
|
+
|
|
273
|
+
def count_facts(self, provider_id: UUID | None = None) -> int:
|
|
274
|
+
with self._lock:
|
|
275
|
+
col = self._map.collection_name("activity_facts")
|
|
276
|
+
f_pid = self._map.field_name("provider_id")
|
|
277
|
+
if provider_id is not None:
|
|
278
|
+
cursor = self._db.aql.execute(
|
|
279
|
+
f"RETURN LENGTH("
|
|
280
|
+
f" FOR doc IN {col} "
|
|
281
|
+
f" FILTER doc.{f_pid} == @provider_id "
|
|
282
|
+
f" RETURN 1"
|
|
283
|
+
f")",
|
|
284
|
+
bind_vars={"provider_id": str(provider_id)},
|
|
285
|
+
)
|
|
286
|
+
else:
|
|
287
|
+
cursor = self._db.aql.execute(
|
|
288
|
+
f"RETURN LENGTH({col})"
|
|
289
|
+
)
|
|
290
|
+
results = list(cursor)
|
|
291
|
+
return results[0] if results else 0
|
|
292
|
+
|
|
293
|
+
# -- Internal helpers ──────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
def _doc_to_fact(self, doc: dict) -> FactRecord:
|
|
296
|
+
"""Convert an ArangoDB document to a FactRecord."""
|
|
297
|
+
# Reverse-map field names, then strip ArangoDB metadata
|
|
298
|
+
deobfuscated = self._map.deobfuscate_document(doc)
|
|
299
|
+
data = {k: v for k, v in deobfuscated.items() if not k.startswith("_")}
|
|
300
|
+
data["id"] = doc["_key"]
|
|
301
|
+
return FactRecord.model_validate(data)
|
|
302
|
+
|
|
303
|
+
def _doc_to_anchor(self, doc: dict) -> MemoryAnchor:
|
|
304
|
+
"""Convert an ArangoDB document to a MemoryAnchor."""
|
|
305
|
+
deobfuscated = self._map.deobfuscate_document(doc)
|
|
306
|
+
data = {k: v for k, v in deobfuscated.items() if not k.startswith("_")}
|
|
307
|
+
data["handle"] = doc["_key"]
|
|
308
|
+
return MemoryAnchor.model_validate(data)
|