open-kgo 0.2.1__py3-none-any.whl

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.
Files changed (133) hide show
  1. open_kgo/__init__.py +0 -0
  2. open_kgo/compute_frameworks/__init__.py +16 -0
  3. open_kgo/compute_frameworks/python_dict_kg_framework.py +93 -0
  4. open_kgo/compute_frameworks/tests/__init__.py +0 -0
  5. open_kgo/compute_frameworks/tests/test_python_dict_kg_framework.py +111 -0
  6. open_kgo/extenders/__init__.py +1 -0
  7. open_kgo/feature_groups/__init__.py +1 -0
  8. open_kgo/feature_groups/kg/__init__.py +7 -0
  9. open_kgo/feature_groups/kg/agent_memory/__init__.py +33 -0
  10. open_kgo/feature_groups/kg/agent_memory/base.py +102 -0
  11. open_kgo/feature_groups/kg/agent_memory/graph_walk_memory.py +111 -0
  12. open_kgo/feature_groups/kg/agent_memory/networkx_memory.py +106 -0
  13. open_kgo/feature_groups/kg/agent_memory/shared.py +97 -0
  14. open_kgo/feature_groups/kg/agent_memory/tests/__init__.py +0 -0
  15. open_kgo/feature_groups/kg/agent_memory/tests/kg_agent_memory_contract.py +21 -0
  16. open_kgo/feature_groups/kg/agent_memory/tests/test_graph_walk_memory.py +262 -0
  17. open_kgo/feature_groups/kg/agent_memory/tests/test_networkx_memory.py +125 -0
  18. open_kgo/feature_groups/kg/base.py +40 -0
  19. open_kgo/feature_groups/kg/citation_rest/__init__.py +21 -0
  20. open_kgo/feature_groups/kg/citation_rest/base.py +47 -0
  21. open_kgo/feature_groups/kg/citation_rest/file_fixture_citation.py +82 -0
  22. open_kgo/feature_groups/kg/citation_rest/paginated_citation.py +104 -0
  23. open_kgo/feature_groups/kg/citation_rest/tests/__init__.py +0 -0
  24. open_kgo/feature_groups/kg/citation_rest/tests/kg_citation_rest_contract.py +15 -0
  25. open_kgo/feature_groups/kg/citation_rest/tests/test_file_fixture_citation.py +110 -0
  26. open_kgo/feature_groups/kg/citation_rest/tests/test_paginated_citation.py +250 -0
  27. open_kgo/feature_groups/kg/class_guards.py +196 -0
  28. open_kgo/feature_groups/kg/code_build/__init__.py +36 -0
  29. open_kgo/feature_groups/kg/code_build/base.py +52 -0
  30. open_kgo/feature_groups/kg/code_build/cyclonedx_sbom.py +60 -0
  31. open_kgo/feature_groups/kg/code_build/spdx_sbom.py +195 -0
  32. open_kgo/feature_groups/kg/code_build/tests/__init__.py +0 -0
  33. open_kgo/feature_groups/kg/code_build/tests/kg_code_build_contract.py +15 -0
  34. open_kgo/feature_groups/kg/code_build/tests/test_cyclonedx_sbom.py +57 -0
  35. open_kgo/feature_groups/kg/code_build/tests/test_spdx_sbom.py +391 -0
  36. open_kgo/feature_groups/kg/conftest.py +52 -0
  37. open_kgo/feature_groups/kg/credentials.py +257 -0
  38. open_kgo/feature_groups/kg/embedded/__init__.py +15 -0
  39. open_kgo/feature_groups/kg/embedded/base.py +100 -0
  40. open_kgo/feature_groups/kg/embedded/igraph_embedded.py +145 -0
  41. open_kgo/feature_groups/kg/embedded/networkx_embedded.py +104 -0
  42. open_kgo/feature_groups/kg/embedded/tests/__init__.py +0 -0
  43. open_kgo/feature_groups/kg/embedded/tests/kg_embedded_contract.py +71 -0
  44. open_kgo/feature_groups/kg/embedded/tests/test_igraph_embedded.py +149 -0
  45. open_kgo/feature_groups/kg/embedded/tests/test_networkx_embedded.py +60 -0
  46. open_kgo/feature_groups/kg/errors.py +164 -0
  47. open_kgo/feature_groups/kg/feature_group.py +54 -0
  48. open_kgo/feature_groups/kg/fixtures.py +371 -0
  49. open_kgo/feature_groups/kg/lineage/__init__.py +18 -0
  50. open_kgo/feature_groups/kg/lineage/base.py +35 -0
  51. open_kgo/feature_groups/kg/lineage/dbt_manifest.py +116 -0
  52. open_kgo/feature_groups/kg/lineage/openlineage_events.py +195 -0
  53. open_kgo/feature_groups/kg/lineage/tests/__init__.py +0 -0
  54. open_kgo/feature_groups/kg/lineage/tests/kg_lineage_contract.py +21 -0
  55. open_kgo/feature_groups/kg/lineage/tests/test_dbt_manifest.py +151 -0
  56. open_kgo/feature_groups/kg/lineage/tests/test_openlineage_events.py +415 -0
  57. open_kgo/feature_groups/kg/mixins.py +301 -0
  58. open_kgo/feature_groups/kg/network_pg/__init__.py +13 -0
  59. open_kgo/feature_groups/kg/network_pg/base.py +51 -0
  60. open_kgo/feature_groups/kg/network_pg/grand_cypher.py +170 -0
  61. open_kgo/feature_groups/kg/network_pg/kuzu_cypher.py +81 -0
  62. open_kgo/feature_groups/kg/network_pg/tests/__init__.py +0 -0
  63. open_kgo/feature_groups/kg/network_pg/tests/kg_network_pg_contract.py +14 -0
  64. open_kgo/feature_groups/kg/network_pg/tests/test_grand_cypher.py +237 -0
  65. open_kgo/feature_groups/kg/network_pg/tests/test_kuzu_cypher.py +113 -0
  66. open_kgo/feature_groups/kg/ontology/__init__.py +4 -0
  67. open_kgo/feature_groups/kg/ontology/registry.py +195 -0
  68. open_kgo/feature_groups/kg/ontology/semantic_field.py +373 -0
  69. open_kgo/feature_groups/kg/ontology/tests/__init__.py +0 -0
  70. open_kgo/feature_groups/kg/ontology/tests/test_ontology_integration.py +273 -0
  71. open_kgo/feature_groups/kg/ontology/tests/test_registry.py +176 -0
  72. open_kgo/feature_groups/kg/ontology/tests/test_semantic_field.py +194 -0
  73. open_kgo/feature_groups/kg/ontology/tests/test_semantic_field_scale.py +209 -0
  74. open_kgo/feature_groups/kg/rdf/__init__.py +17 -0
  75. open_kgo/feature_groups/kg/rdf/base.py +64 -0
  76. open_kgo/feature_groups/kg/rdf/oxigraph_sparql.py +141 -0
  77. open_kgo/feature_groups/kg/rdf/rdflib_sparql.py +102 -0
  78. open_kgo/feature_groups/kg/rdf/tests/__init__.py +0 -0
  79. open_kgo/feature_groups/kg/rdf/tests/kg_rdf_contract.py +39 -0
  80. open_kgo/feature_groups/kg/rdf/tests/test_oxigraph_sparql.py +260 -0
  81. open_kgo/feature_groups/kg/rdf/tests/test_rdflib_sparql.py +121 -0
  82. open_kgo/feature_groups/kg/reader_base.py +792 -0
  83. open_kgo/feature_groups/kg/readers.py +252 -0
  84. open_kgo/feature_groups/kg/rest_public/__init__.py +26 -0
  85. open_kgo/feature_groups/kg/rest_public/base.py +42 -0
  86. open_kgo/feature_groups/kg/rest_public/file_fixture_paged_rest.py +127 -0
  87. open_kgo/feature_groups/kg/rest_public/file_fixture_rest.py +112 -0
  88. open_kgo/feature_groups/kg/rest_public/tests/__init__.py +0 -0
  89. open_kgo/feature_groups/kg/rest_public/tests/kg_rest_public_contract.py +15 -0
  90. open_kgo/feature_groups/kg/rest_public/tests/test_file_fixture_paged_rest.py +203 -0
  91. open_kgo/feature_groups/kg/rest_public/tests/test_file_fixture_rest.py +120 -0
  92. open_kgo/feature_groups/kg/saas_authz/__init__.py +20 -0
  93. open_kgo/feature_groups/kg/saas_authz/base.py +62 -0
  94. open_kgo/feature_groups/kg/saas_authz/in_process_tuple_store.py +191 -0
  95. open_kgo/feature_groups/kg/saas_authz/paginated_tuple_store.py +201 -0
  96. open_kgo/feature_groups/kg/saas_authz/shared.py +37 -0
  97. open_kgo/feature_groups/kg/saas_authz/tests/__init__.py +0 -0
  98. open_kgo/feature_groups/kg/saas_authz/tests/kg_saas_authz_contract.py +10 -0
  99. open_kgo/feature_groups/kg/saas_authz/tests/test_in_process_tuple_store.py +94 -0
  100. open_kgo/feature_groups/kg/saas_authz/tests/test_paginated_tuple_store.py +347 -0
  101. open_kgo/feature_groups/kg/spec.py +103 -0
  102. open_kgo/feature_groups/kg/tests/__init__.py +0 -0
  103. open_kgo/feature_groups/kg/tests/_discovery.py +252 -0
  104. open_kgo/feature_groups/kg/tests/_family_cases.py +474 -0
  105. open_kgo/feature_groups/kg/tests/_helpers.py +211 -0
  106. open_kgo/feature_groups/kg/tests/contract_adapters.py +67 -0
  107. open_kgo/feature_groups/kg/tests/contract_credentials.py +419 -0
  108. open_kgo/feature_groups/kg/tests/contract_load.py +160 -0
  109. open_kgo/feature_groups/kg/tests/contract_params.py +185 -0
  110. open_kgo/feature_groups/kg/tests/contract_surface.py +98 -0
  111. open_kgo/feature_groups/kg/tests/kg_contract.py +67 -0
  112. open_kgo/feature_groups/kg/tests/test_abstract_base_and_result_limit_bounds.py +269 -0
  113. open_kgo/feature_groups/kg/tests/test_base_validation.py +313 -0
  114. open_kgo/feature_groups/kg/tests/test_cross_group_contract.py +96 -0
  115. open_kgo/feature_groups/kg/tests/test_cross_layer_pagination.py +231 -0
  116. open_kgo/feature_groups/kg/tests/test_discovery_helpers.py +354 -0
  117. open_kgo/feature_groups/kg/tests/test_helpers.py +321 -0
  118. open_kgo/feature_groups/kg/tests/test_kg_catalog_declarations.py +131 -0
  119. open_kgo/feature_groups/kg/tests/test_kg_registry_integrity.py +76 -0
  120. open_kgo/feature_groups/kg/tests/test_kg_usage_smoke.py +34 -0
  121. open_kgo/feature_groups/kg/tests/test_property_mapping_integrity.py +732 -0
  122. open_kgo/feature_groups/kg/tests/test_resource_cache.py +417 -0
  123. open_kgo/feature_groups/kg/tests/test_spec.py +88 -0
  124. open_kgo/feature_groups/kg/tests/test_validate_params_collect_all.py +113 -0
  125. open_kgo/feature_groups/kg/tests/test_validation_contract.py +225 -0
  126. open_kgo/feature_groups/kg/traversal.py +125 -0
  127. open_kgo/feature_groups/kg/validation.py +60 -0
  128. open_kgo/py.typed +0 -0
  129. open_kgo-0.2.1.dist-info/METADATA +264 -0
  130. open_kgo-0.2.1.dist-info/RECORD +133 -0
  131. open_kgo-0.2.1.dist-info/WHEEL +5 -0
  132. open_kgo-0.2.1.dist-info/licenses/LICENSE +201 -0
  133. open_kgo-0.2.1.dist-info/top_level.txt +1 -0
open_kgo/__init__.py ADDED
File without changes
@@ -0,0 +1,16 @@
1
+ """Compute frameworks for open-kgo.
2
+
3
+ Currently holds ``KgPythonDictFramework`` (``python_dict_kg_framework.py``), the
4
+ KG-aware ``PythonDictFramework`` adapter that wraps native KG rows as
5
+ ``{feature_name: row}`` before column slicing. It is pinned by
6
+ ``open_kgo.feature_groups.kg.base.KgConnectorFeatureGroupBase.compute_framework_rule``
7
+ so every KG FeatureGroup runs through it by default.
8
+
9
+ Re-exported at the package root so it is discoverable straight from
10
+ ``compute_frameworks`` (``from open_kgo.compute_frameworks import
11
+ KgPythonDictFramework``) without reaching into the submodule.
12
+ """
13
+
14
+ from open_kgo.compute_frameworks.python_dict_kg_framework import KgPythonDictFramework
15
+
16
+ __all__ = ["KgPythonDictFramework"]
@@ -0,0 +1,93 @@
1
+ """KG-aware adapter over ``PythonDictFramework``.
2
+
3
+ Native KG rows (SPARQL bindings, Cypher rows, REST records, BFS hops, ...)
4
+ have their own keys (``s``, ``p``, ``o``, ``name``, ``ancestor``, ...) — none
5
+ of which match the user-defined feature name. ``PythonDictFramework`` matches
6
+ columns by feature-name presence in each row (see
7
+ ``select_data_by_column_names`` in
8
+ ``mloda_plugins.compute_framework.base_implementations.python_dict``), so an
9
+ unmatched feature would yield ``[{}, {}, ...]`` and silently lose every row.
10
+
11
+ The wrap that satisfies that contract used to live on the universal
12
+ ``KgConnectorReaderBase.load``, which made the framework-matching strategy
13
+ leak into every reader regardless of compute framework. This adapter moves
14
+ the wrap into a ``PythonDictFramework`` subclass that owns it as an internal
15
+ concern: ``load_data`` returns native KG rows; this framework wraps them as
16
+ ``{feature_name: row}`` immediately before column slicing. Other compute
17
+ frameworks never see the wrap.
18
+
19
+ Pinned by ``KgConnectorFeatureGroupBase.compute_framework_rule`` so every KG
20
+ FeatureGroup runs through this adapter by default.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, Optional
26
+
27
+ from mloda.user import FeatureName
28
+ from mloda_plugins.compute_framework.base_implementations.python_dict.python_dict_framework import (
29
+ PythonDictFramework,
30
+ )
31
+
32
+
33
+ class KgPythonDictFramework(PythonDictFramework):
34
+ """``PythonDictFramework`` that wraps native KG rows under the requested feature name.
35
+
36
+ Mirrors the prior universal-base behavior: every row is wrapped as
37
+ ``{feature_name: row}`` before column slicing, regardless of whether
38
+ ``feature_name`` already appears as a key in the row. Wrapping
39
+ unconditionally preserves the historical contract, a row whose own keys
40
+ happen to collide with the feature name still loses access to the
41
+ colliding key, but that was already the case under the prior wrap and
42
+ changing it here would be a silent semantic shift.
43
+
44
+ The parent's ``identify_naming_convention`` hook (which supports the
45
+ ``feature_name~suffix`` multi-column pattern) is intentionally bypassed:
46
+ after the wrap, every row has exactly one key, the feature name, so
47
+ convention-based suffix matching has nothing to expand against. Native KG
48
+ rows have their own keys (``s``, ``p``, ``o``, ...) that never match the
49
+ user-defined feature name with or without suffixes, so this restriction
50
+ matches existing reality. KG features that need the suffix pattern would
51
+ need a different adapter; surfacing that explicitly is preferable to
52
+ silent loss.
53
+
54
+ Empty results are returned as ``[]`` instead of raising. The
55
+ parent ``PythonDictFramework`` treats ``[]`` as fatal because for tabular
56
+ data an empty selection is usually a schema-discovery failure (no rows
57
+ means no column types to infer, no naming convention to expand). KG
58
+ semantics are different: a SPARQL ``SELECT`` with no matches, a citation
59
+ lookup against an unknown ``stable_id``, an ``agent_memory`` query that
60
+ finds nothing, or a ``saas_authz`` filter that excludes every tuple all
61
+ legitimately return zero rows. Forcing those onto the parent's
62
+ "empty is fatal" path made ``mloda.run_all`` hostile to data users with
63
+ real empty-result queries (the prior workaround had to bypass the run
64
+ entirely). The cardinality and ``column_ordering`` guards still fire
65
+ even on empty data, because those are caller-shape bugs that are
66
+ orthogonal to row count.
67
+ """
68
+
69
+ def select_data_by_column_names(
70
+ self,
71
+ data: list[dict[str, Any]],
72
+ selected_feature_names: set[FeatureName],
73
+ column_ordering: Optional[str] = None,
74
+ ) -> list[dict[str, Any]]:
75
+ if len(selected_feature_names) != 1:
76
+ raise ValueError(
77
+ f"{type(self).__name__}.select_data_by_column_names expects exactly one feature per call "
78
+ f"(KG concrete load_data implementations all dispatch one feature at a time); "
79
+ f"the reader's ``load`` should reject this earlier, this branch is defense-in-depth. "
80
+ f"Got {sorted(str(n) for n in selected_feature_names)}."
81
+ )
82
+
83
+ if column_ordering is not None:
84
+ raise ValueError(
85
+ f"{type(self).__name__} does not honor column_ordering "
86
+ f"(single-feature wrap has no ordering to apply); got {column_ordering!r}."
87
+ )
88
+
89
+ if not data:
90
+ return []
91
+
92
+ feature_name = str(next(iter(selected_feature_names)))
93
+ return [{feature_name: row} for row in data]
File without changes
@@ -0,0 +1,111 @@
1
+ """Direct unit tests for ``KgPythonDictFramework``.
2
+
3
+ The KG-aware ``PythonDictFramework`` adapter wraps native KG rows under the
4
+ requested feature name during column slicing, so the wrap-for-column-matching
5
+ strategy lives in a framework-specific layer instead of leaking into the
6
+ universal ``KgConnectorReaderBase.load``. These tests pin down the adapter's
7
+ public behaviour independently of any concrete connector.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import pytest
13
+
14
+ from mloda.core.abstract_plugins.components.parallelization_modes import ParallelizationMode
15
+ from mloda.user import FeatureName
16
+
17
+ from open_kgo.compute_frameworks.python_dict_kg_framework import KgPythonDictFramework
18
+
19
+
20
+ def _make_framework() -> KgPythonDictFramework:
21
+ """Construct an instance with the bare minimum mloda runtime args.
22
+
23
+ Tracks ``ComputeFramework.__init__`` (private mloda surface): currently
24
+ requires ``mode`` and ``children_if_root``. These tests exercise pure
25
+ data-shaping behaviour, not parallelization or DAG wiring, so any sync
26
+ mode and an empty children set suffice. If mloda renames either kwarg or
27
+ adds a required one, every test here fails together — fix the helper.
28
+ """
29
+ return KgPythonDictFramework(mode=ParallelizationMode.SYNC, children_if_root=frozenset())
30
+
31
+
32
+ def test_select_data_wraps_native_rows_under_feature_name() -> None:
33
+ """Each native row is returned as ``{feature_name: row}``."""
34
+ fw = _make_framework()
35
+ native_rows = [{"s": "x", "p": "y", "o": "z"}, {"s": "a", "p": "b", "o": "c"}]
36
+ result = fw.select_data_by_column_names(native_rows, {FeatureName("my_feature")})
37
+ assert result == [
38
+ {"my_feature": {"s": "x", "p": "y", "o": "z"}},
39
+ {"my_feature": {"s": "a", "p": "b", "o": "c"}},
40
+ ]
41
+
42
+
43
+ def test_select_data_wrap_is_unconditional_even_when_feature_name_collides() -> None:
44
+ """Wrap mirrors the prior ``KgConnectorReaderBase.load`` semantics: always wrap.
45
+
46
+ Conditional wrapping (skip when the feature name already appears as a
47
+ row key) would silently change behaviour for the rare collision case.
48
+ Wrapping unconditionally preserves the historical contract.
49
+ """
50
+ fw = _make_framework()
51
+ rows = [{"my_feature": "shadowed", "other": 1}]
52
+ result = fw.select_data_by_column_names(rows, {FeatureName("my_feature")})
53
+ assert result == [{"my_feature": {"my_feature": "shadowed", "other": 1}}]
54
+
55
+
56
+ def test_select_data_rejects_multi_feature_call() -> None:
57
+ """Adapter mirrors the reader's single-feature contract.
58
+
59
+ KG readers dispatch one feature at a time; passing two feature names to
60
+ the framework's column slicer would silently pick one. Reject loudly.
61
+ """
62
+ fw = _make_framework()
63
+ with pytest.raises(ValueError):
64
+ fw.select_data_by_column_names([{"a": 1}], {FeatureName("feat_a"), FeatureName("feat_b")})
65
+
66
+
67
+ def test_select_data_returns_empty_list_for_empty_input() -> None:
68
+ """Empty input returns ``[]``.
69
+
70
+ The parent ``PythonDictFramework`` raises on ``[]`` because for tabular
71
+ data an empty selection is usually a schema-discovery failure. KG
72
+ semantics differ: a query with no matches is a legitimate outcome, so
73
+ the adapter relaxes the parent's "empty is fatal" guard. The
74
+ cardinality and ``column_ordering`` guards are orthogonal and still
75
+ fire (see the dedicated tests below); this test only pins the
76
+ relaxation on the data dimension.
77
+ """
78
+ fw = _make_framework()
79
+ result = fw.select_data_by_column_names([], {FeatureName("my_feature")})
80
+ assert result == []
81
+
82
+
83
+ def test_select_data_empty_input_still_rejects_multi_feature_call() -> None:
84
+ """Cardinality guard fires even on ``[]`` (caller-shape bug is orthogonal to row count)."""
85
+ fw = _make_framework()
86
+ with pytest.raises(ValueError):
87
+ fw.select_data_by_column_names([], {FeatureName("feat_a"), FeatureName("feat_b")})
88
+
89
+
90
+ def test_select_data_empty_input_still_rejects_column_ordering() -> None:
91
+ """``column_ordering`` guard fires even on ``[]`` (caller-shape bug is orthogonal to row count)."""
92
+ fw = _make_framework()
93
+ with pytest.raises(ValueError):
94
+ fw.select_data_by_column_names([], {FeatureName("my_feature")}, column_ordering="alphabetical")
95
+
96
+
97
+ def test_select_data_rejects_column_ordering() -> None:
98
+ """``column_ordering`` is meaningless for a single-feature wrap; reject loudly.
99
+
100
+ The parent ``PythonDictFramework`` honors ``column_ordering`` via
101
+ ``identify_naming_convention``. The KG adapter bypasses that hook because
102
+ each wrapped row has exactly one key (the feature name). Silently
103
+ accepting the parameter would be a lie about supported semantics.
104
+ """
105
+ fw = _make_framework()
106
+ with pytest.raises(ValueError):
107
+ fw.select_data_by_column_names(
108
+ [{"a": 1}],
109
+ {FeatureName("my_feature")},
110
+ column_ordering="alphabetical",
111
+ )
@@ -0,0 +1 @@
1
+ """Extenders namespace package."""
@@ -0,0 +1 @@
1
+ """Feature groups namespace package."""
@@ -0,0 +1,7 @@
1
+ """Knowledge-graph connector base groups (prototype).
2
+
3
+ Validates the property layout from `docs/kg-connector-base-classes.md` Version B
4
+ against in-memory libraries and Python prototype implementations. No Docker, no
5
+ external services. See `docs/kg-connector-config-examples.md` for credential
6
+ shapes per family.
7
+ """
@@ -0,0 +1,33 @@
1
+ """LLM agent memory / GraphRAG KG connectors (Letta, Zep+Graphiti, Mem0, ...).
2
+
3
+ Hidden-KG family with bi-temporal semantics. Required-with-at-least-one
4
+ ``memory_scope_*`` family of properties; ``valid_at_range`` /
5
+ ``invalid_at_range`` / ``reference_time`` for bi-temporal queries; lexical and
6
+ graph retrieval modes are implemented (vector / hybrid remain unimplemented).
7
+
8
+ PROTOTYPE NOTE: ``NetworkxMemoryReader`` loads its in-process substrate from
9
+ a JSON fixture pointed to by the ``locator`` credential slot
10
+ (``tests/fixtures/memories.json`` ships ``user_42``). A
11
+ ``memory_scope_user_id`` value absent from the fixture raises
12
+ ``UnknownMemoryScopeError`` at ``connect()`` time rather than silently
13
+ returning an empty graph; an unreadable / malformed locator raises
14
+ ``FixtureLoadError``. The concrete reader narrows the family-level
15
+ ``REQUIRED_KEYS`` to ``("memory_scope_user_id",)`` because the fixture
16
+ format is keyed by user_id only — the other ``memory_scope_*`` aliases
17
+ live on the family base for future concrete readers (Mem0, Zep+Graphiti,
18
+ Letta) that own their own per-tenant stores.
19
+
20
+ PROTOTYPE NOTE: ``retrieval_mode`` is strict-validated against
21
+ ``{lexical, vector, hybrid, graph}``. ``NetworkxMemoryReader`` narrows it to
22
+ ``lexical`` (string-match over node labels); ``GraphWalkMemoryReader``
23
+ narrows it to ``graph`` (BFS from a seed node id given as ``query_text``).
24
+ ``vector`` / ``hybrid`` remain unimplemented and are rejected at
25
+ ``is_valid_credentials`` time via ``SUPPORTED_VALUES`` on each concrete.
26
+ ``SUPPORTED_VALUES`` only validates keys present in the slot, and the family
27
+ default is ``lexical``, so ``GraphWalkMemoryReader`` additionally lists
28
+ ``retrieval_mode`` in ``REQUIRED_KEYS``: omitting the key (which would
29
+ silently default to the un-honored ``lexical``) is rejected at
30
+ ``is_valid_credentials`` time as well. Together the two layers ensure neither
31
+ reader lies about what it honors (``NetworkxMemoryReader`` honors the family
32
+ default, so omission is safe there and the key stays optional).
33
+ """
@@ -0,0 +1,102 @@
1
+ """Family base for agent memory / GraphRAG connectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, ClassVar
6
+
7
+ from open_kgo.feature_groups.kg.base import KgConnectorFeatureGroupBase, QueryReader
8
+ from open_kgo.feature_groups.kg.mixins import PaginationMixin
9
+ from open_kgo.feature_groups.kg.spec import property_spec
10
+
11
+
12
+ _RETRIEVAL_MODES: dict[str, str] = {
13
+ "lexical": "BM25-style lexical search.",
14
+ "vector": "Embedding similarity search.",
15
+ "hybrid": "Combined lexical + vector with mmr_lambda blend.",
16
+ "graph": "Pure graph-walk retrieval.",
17
+ }
18
+
19
+
20
+ # Single source of truth for the memory-scope key family. Drives both
21
+ # ``MEMORY_SCOPE_KEYS`` and the property-mapping entries below, so renaming
22
+ # or extending the scope happens in exactly one place.
23
+ #
24
+ # Note on consumers: the existing ``NetworkxMemoryReader`` narrows
25
+ # ``REQUIRED_KEYS`` to ``("memory_scope_user_id",)`` only — its JSON fixture
26
+ # is keyed by user_id and the other scope aliases would silently no-op. The
27
+ # canonical OR-group ``MEMORY_SCOPE_KEYS`` is reserved for future concretes
28
+ # (Mem0, Letta, Zep+Graphiti) whose backends honor the full scope; keeping
29
+ # the constant exported documents that contract for the family.
30
+ #
31
+ # Uniformity assumption: every scope key gets ``context=True`` and
32
+ # ``strict_validation=False``. The first scope key that needs a different
33
+ # pair (e.g. ``strict_validation=True`` for an enum scope) will force this
34
+ # tuple shape to grow — at that point switch to a per-key spec dict
35
+ # constant rather than threading more positional fields through.
36
+ _MEMORY_SCOPE_SPECS: tuple[tuple[str, str, None | tuple[()]], ...] = (
37
+ ("memory_scope_user_id", "User identifier scope (Mem0 user_id, Letta user_id).", None),
38
+ ("memory_scope_agent_id", "Agent identifier scope.", None),
39
+ ("memory_scope_session_id", "Session identifier scope (e.g. LangGraph thread_id).", None),
40
+ ("memory_scope_run_id", "Run identifier scope.", None),
41
+ ("memory_scope_group_ids", "Graphiti-style group_ids (list).", ()),
42
+ )
43
+
44
+ MEMORY_SCOPE_KEYS: tuple[str, ...] = tuple(name for name, _, _ in _MEMORY_SCOPE_SPECS)
45
+
46
+ _MEMORY_SCOPE_PROPERTY_MAPPING: dict[str, Any] = {
47
+ name: property_spec(explanation, default=default) for name, explanation, default in _MEMORY_SCOPE_SPECS
48
+ }
49
+
50
+ _FAMILY_PROPERTIES: dict[str, Any] = {
51
+ **_MEMORY_SCOPE_PROPERTY_MAPPING,
52
+ "reference_time": property_spec(
53
+ "Bi-temporal reference time (ISO 8601).",
54
+ ),
55
+ "valid_at_range": property_spec(
56
+ "[start, end] for valid_at filter.",
57
+ default=(),
58
+ ),
59
+ "invalid_at_range": property_spec(
60
+ "[start, end] for invalid_at filter.",
61
+ default=(),
62
+ ),
63
+ "retrieval_mode": property_spec(
64
+ "Retrieval strategy used to score candidate memories.",
65
+ strict=True,
66
+ allowed_values=_RETRIEVAL_MODES,
67
+ default="lexical",
68
+ ),
69
+ "mmr_lambda": property_spec(
70
+ "MMR lambda for hybrid retrieval blend (0.0-1.0).",
71
+ default=0.5,
72
+ ),
73
+ "threshold": property_spec(
74
+ "Similarity threshold (0.0-1.0).",
75
+ default=0.0,
76
+ ),
77
+ }
78
+
79
+
80
+ class AgentMemoryReader(PaginationMixin, QueryReader, family_properties=_FAMILY_PROPERTIES):
81
+ # Honest surface (option 3, see base.py): forward-compat GraphRAG menu the
82
+ # in-process concretes don't read (alternate scope aliases, bi-temporal and
83
+ # scoring knobs, pagination), reserved for future backends (Mem0, Letta,
84
+ # Zep). ``valid_at_range`` is waived per-concrete on GraphWalkMemoryReader,
85
+ # since the lexical sibling reads it.
86
+ _WAIVED_UNCONSUMED_KEYS: ClassVar[frozenset[str]] = frozenset(
87
+ {
88
+ "page_size",
89
+ "memory_scope_agent_id",
90
+ "memory_scope_session_id",
91
+ "memory_scope_run_id",
92
+ "memory_scope_group_ids",
93
+ "reference_time",
94
+ "invalid_at_range",
95
+ "mmr_lambda",
96
+ "threshold",
97
+ }
98
+ )
99
+
100
+
101
+ class AgentMemoryFeatureGroup(KgConnectorFeatureGroupBase):
102
+ READER_CLASS = None
@@ -0,0 +1,111 @@
1
+ """NetworkX-backed graph-walk agent memory store.
2
+
3
+ Second concrete in the ``agent_memory`` family alongside ``NetworkxMemoryReader``
4
+ (lexical search). This reader implements ``retrieval_mode=graph`` — the pure
5
+ graph-walk retrieval branch the lexical concrete narrows away — by treating the
6
+ ``query_text`` as a seed node id and returning the memories reachable from it
7
+ (BFS over the directed memory graph). It is the family's proof that the
8
+ ``retrieval_mode`` enum's ``graph`` value is real, and it adds no new dependency
9
+ (NetworkX is already a family dependency).
10
+
11
+ Memory data is loaded from a JSON file at ``locator`` (shape:
12
+ ``{user_id: {"nodes": [...], "edges": [...]}}``), the same shape the lexical
13
+ concrete consumes. ``retrieval_mode`` is narrowed to ``graph`` and
14
+ ``pagination_style`` to ``none`` via ``SUPPORTED_VALUES``; ``retrieval_mode``
15
+ is additionally listed in ``REQUIRED_KEYS`` because the family default is
16
+ ``lexical`` (a value this reader does not honor) and ``SUPPORTED_VALUES``
17
+ only checks keys present in the slot, so an omitted ``retrieval_mode`` would
18
+ otherwise validate and silently run graph retrieval under a defaulted
19
+ ``lexical`` label.
20
+
21
+ The family-level bi-temporal keys (``valid_at_range`` / ``invalid_at_range`` /
22
+ ``reference_time``) are ACCEPTED but NOT APPLIED by this graph-walk concrete:
23
+ the BFS returns reachable memories regardless of temporal validity. The lexical
24
+ sibling (``NetworkxMemoryReader``) honors ``valid_at_range``; this one does not
25
+ yet. This mirrors how the family base documents the ``memory_scope_*`` aliases
26
+ as accepted-but-no-op until a concrete owns the corresponding store.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Any, ClassVar, Mapping
32
+
33
+ import networkx as nx
34
+
35
+ from mloda.core.abstract_plugins.components.feature_set import FeatureSet
36
+
37
+ from open_kgo.feature_groups.kg.agent_memory.base import (
38
+ AgentMemoryFeatureGroup,
39
+ AgentMemoryReader,
40
+ )
41
+ from open_kgo.feature_groups.kg.agent_memory.shared import build_memory_graph, validate_user_data
42
+ from open_kgo.feature_groups.kg.base import LoadContext
43
+ from open_kgo.feature_groups.kg.errors import UnknownMemoryScopeError
44
+ from open_kgo.feature_groups.kg.fixtures import load_json_fixture
45
+ from open_kgo.feature_groups.kg.traversal import bfs_collect_ids
46
+
47
+
48
+ class GraphWalkMemoryReader(AgentMemoryReader):
49
+ CONNECTOR_ID: ClassVar[str] = "graph_walk_memory"
50
+ # retrieval_mode is REQUIRED (not just narrowed): the family default is
51
+ # "lexical", which this reader does not honor, and SUPPORTED_VALUES only
52
+ # validates keys present in the slot. An omitted retrieval_mode would
53
+ # otherwise pass is_valid_credentials and silently run graph retrieval
54
+ # under a defaulted "lexical" label.
55
+ REQUIRED_KEYS: ClassVar[tuple[tuple[str, ...], ...]] = (
56
+ ("locator",),
57
+ ("memory_scope_user_id",),
58
+ ("retrieval_mode",),
59
+ )
60
+ SUPPORTED_VALUES: ClassVar[Mapping[str, frozenset[Any]]] = {
61
+ "pagination_style": frozenset({"none"}),
62
+ "retrieval_mode": frozenset({"graph"}),
63
+ }
64
+ # Honest surface (option 3, see base.py): the BFS walk ignores temporal
65
+ # validity, so ``valid_at_range`` is accepted but not applied here (the
66
+ # lexical sibling reads it; the family base waives the rest).
67
+ _WAIVED_UNCONSUMED_KEYS: ClassVar[frozenset[str]] = frozenset({"valid_at_range"})
68
+
69
+ @classmethod
70
+ def _connect_from_slot(cls, slot: Mapping[str, Any]) -> nx.MultiDiGraph:
71
+ locator = str(slot["locator"])
72
+ store = load_json_fixture(cls.CONNECTOR_ID, locator)
73
+ user_id = str(slot["memory_scope_user_id"])
74
+ if user_id not in store:
75
+ raise UnknownMemoryScopeError(cls.CONNECTOR_ID, user_id)
76
+ # require_graph_integrity: this concrete's rows come straight from the
77
+ # BFS walk, so dangling edge endpoints must be rejected at connect time.
78
+ user_data = validate_user_data(cls.CONNECTOR_ID, locator, user_id, store[user_id], require_graph_integrity=True)
79
+ return build_memory_graph(user_data)
80
+
81
+ @classmethod
82
+ def build_query(cls, features: FeatureSet) -> str:
83
+ feature = next(iter(features.features))
84
+ text = feature.options.get("query_text")
85
+ if not isinstance(text, str) or not text.strip():
86
+ raise ValueError(f"{cls.CONNECTOR_ID}: 'query_text' (the seed node id) is required.")
87
+ # Return the stripped seed: validation already requires non-whitespace
88
+ # content, and an unstripped " m1 " would validate here yet match no
89
+ # node in the walk, silently returning [].
90
+ return text.strip()
91
+
92
+ @classmethod
93
+ def _load_rows(cls, ctx: LoadContext, connection: Any, features: FeatureSet) -> list[dict[str, Any]]:
94
+ graph = connection
95
+ seed = cls.build_query(features)
96
+
97
+ # BFS over reachable memories (depth-unbounded); short-circuits at
98
+ # result_limit so the cap bounds the walk rather than slicing a
99
+ # fully-expanded reachable set. A seed absent from the graph yields
100
+ # an empty walk (the contains gate skips it without expanding).
101
+ collected = bfs_collect_ids(
102
+ lambda node_id: node_id in graph,
103
+ graph.successors,
104
+ seed,
105
+ max_nodes=ctx.result_limit,
106
+ )
107
+ return [{"id": node_id, **graph.nodes[node_id]} for node_id in collected]
108
+
109
+
110
+ class GraphWalkMemoryFeatureGroup(AgentMemoryFeatureGroup):
111
+ READER_CLASS: ClassVar[type[GraphWalkMemoryReader]] = GraphWalkMemoryReader # type: ignore[assignment]
@@ -0,0 +1,106 @@
1
+ """NetworkX-backed agent memory store.
2
+
3
+ In-process MultiDiGraph keyed by (node, valid_at, invalid_at). Supports
4
+ ``retrieval_mode=lexical`` (string-match against node labels) without an LLM
5
+ or embedding service. Demonstrates the agent_memory contract shape.
6
+
7
+ PROTOTYPE NOTE: ``retrieval_mode`` is narrowed to ``lexical`` via
8
+ ``SUPPORTED_VALUES``; ``vector`` / ``hybrid`` / ``graph`` are rejected at
9
+ ``is_valid_credentials`` time rather than passing validation and raising
10
+ ``NotImplementedError`` at load. Bi-temporal filtering uses simple
11
+ lexicographic comparison on ISO timestamps.
12
+
13
+ Memory data is loaded from a JSON file pointed to by the ``locator``
14
+ credential slot (shape: ``{user_id: {"nodes": [...], "edges": [...]}}``).
15
+ A user_id absent from the loaded fixture raises ``UnknownMemoryScopeError``
16
+ at ``connect()`` time; an unreadable / malformed locator raises
17
+ ``FixtureLoadError``. Both surface the gap as a typed credential-shape
18
+ error rather than silently emptying the result set or leaking raw IO
19
+ exceptions. Fixture loads are mtime-cached via
20
+ ``kg.fixtures.load_json_fixture``, so repeated ``load_data`` calls do
21
+ not re-read disk.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any, ClassVar, Mapping
27
+
28
+ import networkx as nx
29
+
30
+ from mloda.core.abstract_plugins.components.feature_set import FeatureSet
31
+
32
+ from open_kgo.feature_groups.kg.agent_memory.base import (
33
+ AgentMemoryFeatureGroup,
34
+ AgentMemoryReader,
35
+ )
36
+ from open_kgo.feature_groups.kg.agent_memory.shared import build_memory_graph, validate_user_data
37
+ from open_kgo.feature_groups.kg.base import LoadContext
38
+ from open_kgo.feature_groups.kg.errors import UnknownMemoryScopeError
39
+ from open_kgo.feature_groups.kg.fixtures import load_json_fixture
40
+
41
+
42
+ class NetworkxMemoryReader(AgentMemoryReader):
43
+ CONNECTOR_ID: ClassVar[str] = "networkx_memory"
44
+ # Narrowed from the family-level OR-group ({memory_scope_user_id,
45
+ # memory_scope_agent_id, ..._session_id, ..._run_id, ..._group_ids}) to
46
+ # ``("memory_scope_user_id",)`` only: the JSON fixture is keyed by
47
+ # user_id, so the other aliases would either silently no-op or surface
48
+ # a misleading "user_id not provisioned" error at connect time. Future
49
+ # concrete readers (Mem0, Zep+Graphiti, Letta) honor different scope
50
+ # keys; this concrete pins the one its substrate supports. The
51
+ # ``MEMORY_SCOPE_KEYS`` constant in ``agent_memory/base.py`` remains the
52
+ # canonical scope list for those future readers.
53
+ REQUIRED_KEYS: ClassVar[tuple[tuple[str, ...], ...]] = (
54
+ ("locator",),
55
+ ("memory_scope_user_id",),
56
+ )
57
+ SUPPORTED_VALUES: ClassVar[Mapping[str, frozenset[Any]]] = {
58
+ "pagination_style": frozenset({"none"}),
59
+ "retrieval_mode": frozenset({"lexical"}),
60
+ }
61
+
62
+ @classmethod
63
+ def _connect_from_slot(cls, slot: Mapping[str, Any]) -> nx.MultiDiGraph:
64
+ locator = str(slot["locator"])
65
+ store = load_json_fixture(cls.CONNECTOR_ID, locator)
66
+ # REQUIRED_KEYS now requires memory_scope_user_id specifically (this
67
+ # concrete narrows the family OR-group), so the slot subscript is
68
+ # safe — the validator already enforced presence + truthiness.
69
+ user_id = str(slot["memory_scope_user_id"])
70
+ if user_id not in store:
71
+ raise UnknownMemoryScopeError(cls.CONNECTOR_ID, user_id)
72
+ user_data = validate_user_data(cls.CONNECTOR_ID, locator, user_id, store[user_id])
73
+ return build_memory_graph(user_data)
74
+
75
+ @classmethod
76
+ def build_query(cls, features: FeatureSet) -> str:
77
+ feature = next(iter(features.features))
78
+ text = feature.options.get("query_text")
79
+ if not isinstance(text, str):
80
+ raise ValueError(f"{cls.CONNECTOR_ID}: 'query_text' is required.")
81
+ return text
82
+
83
+ @classmethod
84
+ def _load_rows(cls, ctx: LoadContext, connection: Any, features: FeatureSet) -> list[dict[str, Any]]:
85
+ graph = connection
86
+ query_text = cls.build_query(features).lower()
87
+ valid_range = ctx.slot.get("valid_at_range") or ()
88
+
89
+ rows: list[dict[str, Any]] = []
90
+ for node_id, attrs in graph.nodes(data=True):
91
+ label = str(attrs.get("label", "")).lower()
92
+ if query_text and query_text not in label:
93
+ continue
94
+ if valid_range and len(valid_range) == 2:
95
+ start, end = valid_range[0], valid_range[1]
96
+ node_valid = attrs.get("valid_at")
97
+ if node_valid is not None and not (start <= node_valid <= end):
98
+ continue
99
+ rows.append({"id": node_id, **attrs})
100
+ if len(rows) >= ctx.result_limit:
101
+ break
102
+ return rows
103
+
104
+
105
+ class NetworkxMemoryFeatureGroup(AgentMemoryFeatureGroup):
106
+ READER_CLASS: ClassVar[type[NetworkxMemoryReader]] = NetworkxMemoryReader # type: ignore[assignment]