Skip to content

Commit 683b725

Browse files
authored
Merge pull request #14 from alex-feel/alex-feel-dev
Allow nested JSON structures in metadata
2 parents 44ef494 + 2ca4960 commit 683b725

4 files changed

Lines changed: 460 additions & 4 deletions

File tree

app/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
from pydantic import field_validator
1111
from pydantic import model_validator
1212

13-
# Define MetadataDict directly to avoid import issues
14-
type MetadataDict = dict[str, str | int | float | bool | None]
13+
from app.types import MetadataDict
1514

1615

1716
class SourceType(StrEnum):

app/types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
# Metadata value types - simpler non-recursive type for metadata fields
1313
type MetadataValue = str | int | float | bool | None
1414

15-
# Metadata dictionary type for use in models
16-
type MetadataDict = dict[str, MetadataValue]
15+
# Metadata dictionary type for use in models - supports nested JSON structures
16+
type MetadataDict = dict[str, JsonValue]
1717

1818

1919
# API Response TypedDicts for proper return type annotations

tests/test_metadata_filtering.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import math
56
import time
67
from typing import TYPE_CHECKING
78

@@ -571,3 +572,202 @@ async def test_all_operators(
571572

572573
assert 'entries' in result
573574
assert len(result['entries']) == expected_count
575+
576+
577+
@pytest.mark.integration
578+
@pytest.mark.usefixtures('initialized_server')
579+
class TestNestedJSONMetadata:
580+
"""Test nested JSON structures in metadata."""
581+
582+
@pytest.mark.asyncio
583+
async def test_store_nested_objects(self) -> None:
584+
"""Test storing nested JSON objects in metadata."""
585+
complex_metadata = {
586+
'status': 'active',
587+
'config': {
588+
'database': {
589+
'connection': {
590+
'pool': {'size': 10, 'timeout': 30},
591+
'retry': {'max_attempts': 3, 'backoff': 2.5},
592+
},
593+
},
594+
'cache': {'enabled': True, 'ttl': 300},
595+
},
596+
'user': {'id': 123, 'name': 'Alice Johnson', 'preferences': {'theme': 'dark', 'language': 'en'}},
597+
}
598+
599+
result = await store_context.fn(
600+
thread_id='test_nested_json',
601+
source='agent',
602+
text='Test nested metadata storage',
603+
metadata=complex_metadata,
604+
ctx=None,
605+
)
606+
607+
assert result['success'] is True
608+
assert 'context_id' in result
609+
610+
# Retrieve and verify the metadata is preserved
611+
search_result = await search_context.fn(thread_id='test_nested_json', ctx=None)
612+
assert len(search_result['entries']) == 1
613+
614+
stored_metadata = search_result['entries'][0]['metadata']
615+
assert stored_metadata['status'] == 'active'
616+
assert stored_metadata['config']['database']['connection']['pool']['size'] == 10
617+
assert stored_metadata['config']['database']['connection']['pool']['timeout'] == 30
618+
assert stored_metadata['config']['database']['connection']['retry']['max_attempts'] == 3
619+
assert stored_metadata['config']['database']['connection']['retry']['backoff'] == 2.5
620+
assert stored_metadata['config']['cache']['enabled'] is True
621+
assert stored_metadata['user']['preferences']['theme'] == 'dark'
622+
assert stored_metadata['user']['preferences']['language'] == 'en'
623+
624+
@pytest.mark.asyncio
625+
async def test_store_arrays_in_metadata(self) -> None:
626+
"""Test storing arrays in metadata."""
627+
metadata_with_arrays = {
628+
'tags': ['urgent', 'backend', 'production'],
629+
'priority_levels': [1, 2, 3, 4, 5],
630+
'mixed_array': ['string', 42, math.pi, True, None],
631+
'nested_arrays': [[1, 2], [3, 4], [5, 6]],
632+
}
633+
634+
result = await store_context.fn(
635+
thread_id='test_arrays',
636+
source='agent',
637+
text='Test array metadata',
638+
metadata=metadata_with_arrays,
639+
ctx=None,
640+
)
641+
642+
assert result['success'] is True
643+
644+
# Retrieve and verify arrays are preserved
645+
search_result = await search_context.fn(thread_id='test_arrays', ctx=None)
646+
stored_metadata = search_result['entries'][0]['metadata']
647+
648+
assert stored_metadata['tags'] == ['urgent', 'backend', 'production']
649+
assert stored_metadata['priority_levels'] == [1, 2, 3, 4, 5]
650+
assert stored_metadata['mixed_array'] == ['string', 42, math.pi, True, None]
651+
assert stored_metadata['nested_arrays'] == [[1, 2], [3, 4], [5, 6]]
652+
653+
@pytest.mark.asyncio
654+
async def test_query_nested_paths(self) -> None:
655+
"""Test querying nested JSON paths."""
656+
# Store multiple entries with nested metadata
657+
await store_context.fn(
658+
thread_id='test_nested_paths',
659+
source='agent',
660+
text='Entry 1',
661+
metadata={'user': {'preferences': {'theme': 'dark', 'notifications': {'email': True}}}},
662+
ctx=None,
663+
)
664+
665+
await store_context.fn(
666+
thread_id='test_nested_paths',
667+
source='agent',
668+
text='Entry 2',
669+
metadata={'user': {'preferences': {'theme': 'light', 'notifications': {'email': False}}}},
670+
ctx=None,
671+
)
672+
673+
# Query using nested path
674+
result = await search_context.fn(
675+
thread_id='test_nested_paths',
676+
metadata={'user.preferences.theme': 'dark'},
677+
ctx=None,
678+
)
679+
680+
assert len(result['entries']) == 1
681+
assert result['entries'][0]['text_content'] == 'Entry 1'
682+
assert result['entries'][0]['metadata']['user']['preferences']['theme'] == 'dark'
683+
684+
@pytest.mark.asyncio
685+
async def test_complex_nested_structure(self) -> None:
686+
"""Test very complex nested structure with multiple levels."""
687+
complex_structure = {
688+
'level1': {
689+
'level2': {
690+
'level3': {
691+
'level4': {
692+
'value': 'deeply_nested',
693+
'number': 42,
694+
'array': [1, 2, 3],
695+
'object': {'key': 'value'},
696+
},
697+
},
698+
},
699+
},
700+
'metrics': {
701+
'cpu': 45.5,
702+
'memory': 512,
703+
'disk': {'used': 80.5, 'total': 100.0, 'partitions': ['/dev/sda1', '/dev/sda2']},
704+
},
705+
'features': {
706+
'enabled': ['feature_a', 'feature_b', 'feature_c'],
707+
'disabled': [],
708+
'experimental': {'count': 3, 'names': ['exp_1', 'exp_2', 'exp_3']},
709+
},
710+
}
711+
712+
result = await store_context.fn(
713+
thread_id='test_complex',
714+
source='agent',
715+
text='Complex nested structure test',
716+
metadata=complex_structure,
717+
ctx=None,
718+
)
719+
720+
assert result['success'] is True
721+
722+
# Verify structure is preserved
723+
search_result = await search_context.fn(thread_id='test_complex', ctx=None)
724+
stored_metadata = search_result['entries'][0]['metadata']
725+
726+
# Verify deep nesting
727+
assert stored_metadata['level1']['level2']['level3']['level4']['value'] == 'deeply_nested'
728+
assert stored_metadata['level1']['level2']['level3']['level4']['number'] == 42
729+
assert stored_metadata['level1']['level2']['level3']['level4']['array'] == [1, 2, 3]
730+
assert stored_metadata['level1']['level2']['level3']['level4']['object']['key'] == 'value'
731+
732+
# Verify metrics
733+
assert stored_metadata['metrics']['cpu'] == 45.5
734+
assert stored_metadata['metrics']['disk']['used'] == 80.5
735+
assert stored_metadata['metrics']['disk']['partitions'] == ['/dev/sda1', '/dev/sda2']
736+
737+
# Verify features
738+
assert stored_metadata['features']['enabled'] == ['feature_a', 'feature_b', 'feature_c']
739+
assert stored_metadata['features']['disabled'] == []
740+
assert stored_metadata['features']['experimental']['count'] == 3
741+
742+
@pytest.mark.asyncio
743+
async def test_mixed_flat_and_nested(self) -> None:
744+
"""Test mixing flat and nested metadata structures."""
745+
mixed_metadata = {
746+
'simple_string': 'value',
747+
'simple_int': 42,
748+
'simple_bool': True,
749+
'nested': {'level1': {'level2': 'deep_value'}},
750+
'array': [1, 2, 3],
751+
}
752+
753+
result = await store_context.fn(
754+
thread_id='test_mixed',
755+
source='agent',
756+
text='Mixed flat and nested',
757+
metadata=mixed_metadata,
758+
ctx=None,
759+
)
760+
761+
assert result['success'] is True
762+
763+
# Query using both flat and nested paths
764+
search_result = await search_context.fn(thread_id='test_mixed', metadata={'simple_string': 'value'}, ctx=None)
765+
assert len(search_result['entries']) == 1
766+
767+
# Verify all types are preserved
768+
stored_metadata = search_result['entries'][0]['metadata']
769+
assert stored_metadata['simple_string'] == 'value'
770+
assert stored_metadata['simple_int'] == 42
771+
assert stored_metadata['simple_bool'] is True
772+
assert stored_metadata['nested']['level1']['level2'] == 'deep_value'
773+
assert stored_metadata['array'] == [1, 2, 3]

0 commit comments

Comments
 (0)