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.
- open_kgo/__init__.py +0 -0
- open_kgo/compute_frameworks/__init__.py +16 -0
- open_kgo/compute_frameworks/python_dict_kg_framework.py +93 -0
- open_kgo/compute_frameworks/tests/__init__.py +0 -0
- open_kgo/compute_frameworks/tests/test_python_dict_kg_framework.py +111 -0
- open_kgo/extenders/__init__.py +1 -0
- open_kgo/feature_groups/__init__.py +1 -0
- open_kgo/feature_groups/kg/__init__.py +7 -0
- open_kgo/feature_groups/kg/agent_memory/__init__.py +33 -0
- open_kgo/feature_groups/kg/agent_memory/base.py +102 -0
- open_kgo/feature_groups/kg/agent_memory/graph_walk_memory.py +111 -0
- open_kgo/feature_groups/kg/agent_memory/networkx_memory.py +106 -0
- open_kgo/feature_groups/kg/agent_memory/shared.py +97 -0
- open_kgo/feature_groups/kg/agent_memory/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/agent_memory/tests/kg_agent_memory_contract.py +21 -0
- open_kgo/feature_groups/kg/agent_memory/tests/test_graph_walk_memory.py +262 -0
- open_kgo/feature_groups/kg/agent_memory/tests/test_networkx_memory.py +125 -0
- open_kgo/feature_groups/kg/base.py +40 -0
- open_kgo/feature_groups/kg/citation_rest/__init__.py +21 -0
- open_kgo/feature_groups/kg/citation_rest/base.py +47 -0
- open_kgo/feature_groups/kg/citation_rest/file_fixture_citation.py +82 -0
- open_kgo/feature_groups/kg/citation_rest/paginated_citation.py +104 -0
- open_kgo/feature_groups/kg/citation_rest/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/citation_rest/tests/kg_citation_rest_contract.py +15 -0
- open_kgo/feature_groups/kg/citation_rest/tests/test_file_fixture_citation.py +110 -0
- open_kgo/feature_groups/kg/citation_rest/tests/test_paginated_citation.py +250 -0
- open_kgo/feature_groups/kg/class_guards.py +196 -0
- open_kgo/feature_groups/kg/code_build/__init__.py +36 -0
- open_kgo/feature_groups/kg/code_build/base.py +52 -0
- open_kgo/feature_groups/kg/code_build/cyclonedx_sbom.py +60 -0
- open_kgo/feature_groups/kg/code_build/spdx_sbom.py +195 -0
- open_kgo/feature_groups/kg/code_build/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/code_build/tests/kg_code_build_contract.py +15 -0
- open_kgo/feature_groups/kg/code_build/tests/test_cyclonedx_sbom.py +57 -0
- open_kgo/feature_groups/kg/code_build/tests/test_spdx_sbom.py +391 -0
- open_kgo/feature_groups/kg/conftest.py +52 -0
- open_kgo/feature_groups/kg/credentials.py +257 -0
- open_kgo/feature_groups/kg/embedded/__init__.py +15 -0
- open_kgo/feature_groups/kg/embedded/base.py +100 -0
- open_kgo/feature_groups/kg/embedded/igraph_embedded.py +145 -0
- open_kgo/feature_groups/kg/embedded/networkx_embedded.py +104 -0
- open_kgo/feature_groups/kg/embedded/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/embedded/tests/kg_embedded_contract.py +71 -0
- open_kgo/feature_groups/kg/embedded/tests/test_igraph_embedded.py +149 -0
- open_kgo/feature_groups/kg/embedded/tests/test_networkx_embedded.py +60 -0
- open_kgo/feature_groups/kg/errors.py +164 -0
- open_kgo/feature_groups/kg/feature_group.py +54 -0
- open_kgo/feature_groups/kg/fixtures.py +371 -0
- open_kgo/feature_groups/kg/lineage/__init__.py +18 -0
- open_kgo/feature_groups/kg/lineage/base.py +35 -0
- open_kgo/feature_groups/kg/lineage/dbt_manifest.py +116 -0
- open_kgo/feature_groups/kg/lineage/openlineage_events.py +195 -0
- open_kgo/feature_groups/kg/lineage/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/lineage/tests/kg_lineage_contract.py +21 -0
- open_kgo/feature_groups/kg/lineage/tests/test_dbt_manifest.py +151 -0
- open_kgo/feature_groups/kg/lineage/tests/test_openlineage_events.py +415 -0
- open_kgo/feature_groups/kg/mixins.py +301 -0
- open_kgo/feature_groups/kg/network_pg/__init__.py +13 -0
- open_kgo/feature_groups/kg/network_pg/base.py +51 -0
- open_kgo/feature_groups/kg/network_pg/grand_cypher.py +170 -0
- open_kgo/feature_groups/kg/network_pg/kuzu_cypher.py +81 -0
- open_kgo/feature_groups/kg/network_pg/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/network_pg/tests/kg_network_pg_contract.py +14 -0
- open_kgo/feature_groups/kg/network_pg/tests/test_grand_cypher.py +237 -0
- open_kgo/feature_groups/kg/network_pg/tests/test_kuzu_cypher.py +113 -0
- open_kgo/feature_groups/kg/ontology/__init__.py +4 -0
- open_kgo/feature_groups/kg/ontology/registry.py +195 -0
- open_kgo/feature_groups/kg/ontology/semantic_field.py +373 -0
- open_kgo/feature_groups/kg/ontology/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/ontology/tests/test_ontology_integration.py +273 -0
- open_kgo/feature_groups/kg/ontology/tests/test_registry.py +176 -0
- open_kgo/feature_groups/kg/ontology/tests/test_semantic_field.py +194 -0
- open_kgo/feature_groups/kg/ontology/tests/test_semantic_field_scale.py +209 -0
- open_kgo/feature_groups/kg/rdf/__init__.py +17 -0
- open_kgo/feature_groups/kg/rdf/base.py +64 -0
- open_kgo/feature_groups/kg/rdf/oxigraph_sparql.py +141 -0
- open_kgo/feature_groups/kg/rdf/rdflib_sparql.py +102 -0
- open_kgo/feature_groups/kg/rdf/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/rdf/tests/kg_rdf_contract.py +39 -0
- open_kgo/feature_groups/kg/rdf/tests/test_oxigraph_sparql.py +260 -0
- open_kgo/feature_groups/kg/rdf/tests/test_rdflib_sparql.py +121 -0
- open_kgo/feature_groups/kg/reader_base.py +792 -0
- open_kgo/feature_groups/kg/readers.py +252 -0
- open_kgo/feature_groups/kg/rest_public/__init__.py +26 -0
- open_kgo/feature_groups/kg/rest_public/base.py +42 -0
- open_kgo/feature_groups/kg/rest_public/file_fixture_paged_rest.py +127 -0
- open_kgo/feature_groups/kg/rest_public/file_fixture_rest.py +112 -0
- open_kgo/feature_groups/kg/rest_public/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/rest_public/tests/kg_rest_public_contract.py +15 -0
- open_kgo/feature_groups/kg/rest_public/tests/test_file_fixture_paged_rest.py +203 -0
- open_kgo/feature_groups/kg/rest_public/tests/test_file_fixture_rest.py +120 -0
- open_kgo/feature_groups/kg/saas_authz/__init__.py +20 -0
- open_kgo/feature_groups/kg/saas_authz/base.py +62 -0
- open_kgo/feature_groups/kg/saas_authz/in_process_tuple_store.py +191 -0
- open_kgo/feature_groups/kg/saas_authz/paginated_tuple_store.py +201 -0
- open_kgo/feature_groups/kg/saas_authz/shared.py +37 -0
- open_kgo/feature_groups/kg/saas_authz/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/saas_authz/tests/kg_saas_authz_contract.py +10 -0
- open_kgo/feature_groups/kg/saas_authz/tests/test_in_process_tuple_store.py +94 -0
- open_kgo/feature_groups/kg/saas_authz/tests/test_paginated_tuple_store.py +347 -0
- open_kgo/feature_groups/kg/spec.py +103 -0
- open_kgo/feature_groups/kg/tests/__init__.py +0 -0
- open_kgo/feature_groups/kg/tests/_discovery.py +252 -0
- open_kgo/feature_groups/kg/tests/_family_cases.py +474 -0
- open_kgo/feature_groups/kg/tests/_helpers.py +211 -0
- open_kgo/feature_groups/kg/tests/contract_adapters.py +67 -0
- open_kgo/feature_groups/kg/tests/contract_credentials.py +419 -0
- open_kgo/feature_groups/kg/tests/contract_load.py +160 -0
- open_kgo/feature_groups/kg/tests/contract_params.py +185 -0
- open_kgo/feature_groups/kg/tests/contract_surface.py +98 -0
- open_kgo/feature_groups/kg/tests/kg_contract.py +67 -0
- open_kgo/feature_groups/kg/tests/test_abstract_base_and_result_limit_bounds.py +269 -0
- open_kgo/feature_groups/kg/tests/test_base_validation.py +313 -0
- open_kgo/feature_groups/kg/tests/test_cross_group_contract.py +96 -0
- open_kgo/feature_groups/kg/tests/test_cross_layer_pagination.py +231 -0
- open_kgo/feature_groups/kg/tests/test_discovery_helpers.py +354 -0
- open_kgo/feature_groups/kg/tests/test_helpers.py +321 -0
- open_kgo/feature_groups/kg/tests/test_kg_catalog_declarations.py +131 -0
- open_kgo/feature_groups/kg/tests/test_kg_registry_integrity.py +76 -0
- open_kgo/feature_groups/kg/tests/test_kg_usage_smoke.py +34 -0
- open_kgo/feature_groups/kg/tests/test_property_mapping_integrity.py +732 -0
- open_kgo/feature_groups/kg/tests/test_resource_cache.py +417 -0
- open_kgo/feature_groups/kg/tests/test_spec.py +88 -0
- open_kgo/feature_groups/kg/tests/test_validate_params_collect_all.py +113 -0
- open_kgo/feature_groups/kg/tests/test_validation_contract.py +225 -0
- open_kgo/feature_groups/kg/traversal.py +125 -0
- open_kgo/feature_groups/kg/validation.py +60 -0
- open_kgo/py.typed +0 -0
- open_kgo-0.2.1.dist-info/METADATA +264 -0
- open_kgo-0.2.1.dist-info/RECORD +133 -0
- open_kgo-0.2.1.dist-info/WHEEL +5 -0
- open_kgo-0.2.1.dist-info/licenses/LICENSE +201 -0
- 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]
|