feat: compound tag filtering via tag_groups (#562)
* feat: add compound tag filtering via tag_groups
Adds tag_groups to RecallRequest and ReflectRequest to express arbitrary
boolean tag predicates: leaf {tags, match}, and/or/not compounds.
Top-level groups are AND-ed. Existing tags/tags_match unchanged.
Examples:
Step filter AND user scope:
tag_groups: [{tags: ["step:5","step:8"], match: "any_strict"},
{tags: ["user:alice"], match: "all_strict"}]
Exclusion:
tag_groups: [{tags: ["user:alice"], match: "all_strict"},
{not: {tags: ["archived"], match: "any_strict"}}]
- Recursive SQL builder (build_tag_groups_where_clause) threads through
all 4 retrieval strategies (semantic/BM25, temporal, graph, MPFP)
- Python-side filter (filter_results_by_tag_groups) for post-traversal
- 22 new unit tests
- OpenAPI spec + all clients regenerated (Rust, Python, TypeScript, Go)
* fix: add tag_groups: None to Rust CLI struct initializers
* fix: add tag_groups: None to Rust client test RecallRequest initializer
* feat: reject tags+tag_groups together, add tag_groups integration tests
- Add model_validator to RecallRequest and ReflectRequest that returns 422
when both `tags` and `tag_groups` are set (mutually exclusive)
- Add 5 integration tests for tag_groups compound filtering:
* validation: 422 when both fields are set
* AND filter: two leaf groups (step scope AND user scope)
* OR compound: user:alice OR user:bob
* NOT compound: user:alice AND NOT archived
* Nested: user:alice AND (step:5 OR step:8)
* ci: trigger CI run
This commit is contained in:
parent
06200f1752
commit
5de793eec7
35 changed files with 3069 additions and 21 deletions
|
|
@ -34,7 +34,7 @@ def _parse_metadata(metadata: Any) -> dict[str, Any]:
|
||||||
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||||
|
|
||||||
from hindsight_api import MemoryEngine
|
from hindsight_api import MemoryEngine
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ def FieldWithDefault(default_factory: Callable, **kwargs) -> Any:
|
||||||
from hindsight_api.config import get_config
|
from hindsight_api.config import get_config
|
||||||
from hindsight_api.engine.memory_engine import Budget, _current_schema, _get_tiktoken_encoding, fq_table
|
from hindsight_api.engine.memory_engine import Budget, _current_schema, _get_tiktoken_encoding, fq_table
|
||||||
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES, MemoryFact, TokenUsage
|
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES, MemoryFact, TokenUsage
|
||||||
from hindsight_api.engine.search.tags import TagsMatch
|
from hindsight_api.engine.search.tags import TagGroup, TagsMatch
|
||||||
from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
|
from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
|
||||||
from hindsight_api.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
|
from hindsight_api.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
|
||||||
from hindsight_api.models import RequestContext
|
from hindsight_api.models import RequestContext
|
||||||
|
|
@ -163,6 +163,17 @@ class RecallRequest(BaseModel):
|
||||||
description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), "
|
description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), "
|
||||||
"'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
"'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
||||||
)
|
)
|
||||||
|
tag_groups: list[TagGroup] | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Compound tag filter using boolean groups. Groups in the list are AND-ed. "
|
||||||
|
"Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_tags_exclusive(self) -> "RecallRequest":
|
||||||
|
if self.tags is not None and self.tag_groups is not None:
|
||||||
|
raise ValueError("'tags' and 'tag_groups' are mutually exclusive. Use 'tag_groups' for compound filtering.")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class RecallResult(BaseModel):
|
class RecallResult(BaseModel):
|
||||||
|
|
@ -639,6 +650,17 @@ class ReflectRequest(BaseModel):
|
||||||
description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), "
|
description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), "
|
||||||
"'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
"'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
||||||
)
|
)
|
||||||
|
tag_groups: list[TagGroup] | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Compound tag filter using boolean groups. Groups in the list are AND-ed. "
|
||||||
|
"Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_tags_exclusive(self) -> "ReflectRequest":
|
||||||
|
if self.tags is not None and self.tag_groups is not None:
|
||||||
|
raise ValueError("'tags' and 'tag_groups' are mutually exclusive. Use 'tag_groups' for compound filtering.")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class ReflectFact(BaseModel):
|
class ReflectFact(BaseModel):
|
||||||
|
|
@ -2324,6 +2346,7 @@ def _register_routes(app: FastAPI):
|
||||||
request_context=request_context,
|
request_context=request_context,
|
||||||
tags=request.tags,
|
tags=request.tags,
|
||||||
tags_match=request.tags_match,
|
tags_match=request.tags_match,
|
||||||
|
tag_groups=request.tag_groups,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert core MemoryFact objects to API RecallResult objects (excluding internal metrics)
|
# Convert core MemoryFact objects to API RecallResult objects (excluding internal metrics)
|
||||||
|
|
@ -2459,6 +2482,7 @@ def _register_routes(app: FastAPI):
|
||||||
request_context=request_context,
|
request_context=request_context,
|
||||||
tags=request.tags,
|
tags=request.tags,
|
||||||
tags_match=request.tags_match,
|
tags_match=request.tags_match,
|
||||||
|
tag_groups=request.tag_groups,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build based_on (memories + mental_models + directives) if facts are requested
|
# Build based_on (memories + mental_models + directives) if facts are requested
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ from .retain import bank_utils, embedding_utils
|
||||||
from .retain.types import RetainContentDict
|
from .retain.types import RetainContentDict
|
||||||
from .search import think_utils
|
from .search import think_utils
|
||||||
from .search.reranking import CrossEncoderReranker, apply_combined_scoring
|
from .search.reranking import CrossEncoderReranker, apply_combined_scoring
|
||||||
from .search.tags import TagsMatch, build_tags_where_clause
|
from .search.tags import TagGroup, TagsMatch, build_tags_where_clause
|
||||||
from .task_backend import BrokerTaskBackend, SyncTaskBackend, TaskBackend
|
from .task_backend import BrokerTaskBackend, SyncTaskBackend, TaskBackend
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2300,6 +2300,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
request_context: "RequestContext",
|
request_context: "RequestContext",
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
_connection_budget: int | None = None,
|
_connection_budget: int | None = None,
|
||||||
_quiet: bool = False,
|
_quiet: bool = False,
|
||||||
) -> RecallResultModel:
|
) -> RecallResultModel:
|
||||||
|
|
@ -2434,6 +2435,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
semaphore_wait=semaphore_wait,
|
semaphore_wait=semaphore_wait,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
connection_budget=_connection_budget,
|
connection_budget=_connection_budget,
|
||||||
quiet=_quiet,
|
quiet=_quiet,
|
||||||
include_source_facts=include_source_facts,
|
include_source_facts=include_source_facts,
|
||||||
|
|
@ -2561,6 +2563,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
semaphore_wait: float = 0.0,
|
semaphore_wait: float = 0.0,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
connection_budget: int | None = None,
|
connection_budget: int | None = None,
|
||||||
quiet: bool = False,
|
quiet: bool = False,
|
||||||
include_source_facts: bool = False,
|
include_source_facts: bool = False,
|
||||||
|
|
@ -2680,6 +2683,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
self.query_analyzer,
|
self.query_analyzer,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
)
|
)
|
||||||
parallel_duration = time.time() - parallel_start
|
parallel_duration = time.time() - parallel_start
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -5040,6 +5044,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
request_context: "RequestContext",
|
request_context: "RequestContext",
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
exclude_mental_model_ids: list[str] | None = None,
|
exclude_mental_model_ids: list[str] | None = None,
|
||||||
_skip_span: bool = False,
|
_skip_span: bool = False,
|
||||||
) -> ReflectResult:
|
) -> ReflectResult:
|
||||||
|
|
@ -5142,6 +5147,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
max_results=max_results,
|
max_results=max_results,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
exclude_ids=exclude_mental_model_ids,
|
exclude_ids=exclude_mental_model_ids,
|
||||||
pending_consolidation=pending_consolidation,
|
pending_consolidation=pending_consolidation,
|
||||||
)
|
)
|
||||||
|
|
@ -5155,6 +5161,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
last_consolidated_at=last_consolidated_at,
|
last_consolidated_at=last_consolidated_at,
|
||||||
pending_consolidation=pending_consolidation,
|
pending_consolidation=pending_consolidation,
|
||||||
)
|
)
|
||||||
|
|
@ -5168,6 +5175,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
max_chunk_tokens=max_chunk_tokens,
|
max_chunk_tokens=max_chunk_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ async def tool_search_mental_models(
|
||||||
max_results: int = 5,
|
max_results: int = 5,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: str = "any",
|
tags_match: str = "any",
|
||||||
|
tag_groups: "list | None" = None,
|
||||||
exclude_ids: list[str] | None = None,
|
exclude_ids: list[str] | None = None,
|
||||||
pending_consolidation: int = 0,
|
pending_consolidation: int = 0,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
@ -52,7 +53,7 @@ async def tool_search_mental_models(
|
||||||
Dict with matching mental models including content and freshness info
|
Dict with matching mental models including content and freshness info
|
||||||
"""
|
"""
|
||||||
from ..memory_engine import fq_table
|
from ..memory_engine import fq_table
|
||||||
from ..search.tags import build_tags_where_clause
|
from ..search.tags import build_tag_groups_where_clause, build_tags_where_clause
|
||||||
|
|
||||||
# Build filters dynamically
|
# Build filters dynamically
|
||||||
filters = ""
|
filters = ""
|
||||||
|
|
@ -65,6 +66,11 @@ async def tool_search_mental_models(
|
||||||
filters += f" {tag_clause}"
|
filters += f" {tag_clause}"
|
||||||
params.extend(tag_params)
|
params.extend(tag_params)
|
||||||
|
|
||||||
|
if tag_groups:
|
||||||
|
groups_clause, groups_params, next_param = build_tag_groups_where_clause(tag_groups, next_param)
|
||||||
|
filters += f" {groups_clause}"
|
||||||
|
params.extend(groups_params)
|
||||||
|
|
||||||
if exclude_ids:
|
if exclude_ids:
|
||||||
filters += f" AND id != ALL(${next_param}::text[])"
|
filters += f" AND id != ALL(${next_param}::text[])"
|
||||||
params.append(exclude_ids)
|
params.append(exclude_ids)
|
||||||
|
|
@ -125,6 +131,7 @@ async def tool_search_observations(
|
||||||
max_tokens: int = 5000,
|
max_tokens: int = 5000,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: str = "any",
|
tags_match: str = "any",
|
||||||
|
tag_groups: "list | None" = None,
|
||||||
last_consolidated_at: datetime | None = None,
|
last_consolidated_at: datetime | None = None,
|
||||||
pending_consolidation: int = 0,
|
pending_consolidation: int = 0,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
@ -157,6 +164,7 @@ async def tool_search_observations(
|
||||||
request_context=request_context,
|
request_context=request_context,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
include_source_facts=True,
|
include_source_facts=True,
|
||||||
max_source_facts_tokens=-1, # No token limit — include all source facts
|
max_source_facts_tokens=-1, # No token limit — include all source facts
|
||||||
_connection_budget=1,
|
_connection_budget=1,
|
||||||
|
|
@ -189,6 +197,7 @@ async def tool_recall(
|
||||||
max_tokens: int = 2048,
|
max_tokens: int = 2048,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: str = "any",
|
tags_match: str = "any",
|
||||||
|
tag_groups: "list | None" = None,
|
||||||
connection_budget: int = 1,
|
connection_budget: int = 1,
|
||||||
max_chunk_tokens: int = 1000,
|
max_chunk_tokens: int = 1000,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
@ -222,6 +231,7 @@ async def tool_recall(
|
||||||
request_context=request_context,
|
request_context=request_context,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
_connection_budget=connection_budget,
|
_connection_budget=connection_budget,
|
||||||
_quiet=True, # Suppress logging for internal operations
|
_quiet=True, # Suppress logging for internal operations
|
||||||
include_chunks=include_chunks,
|
include_chunks=include_chunks,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from ..db_utils import acquire_with_retry
|
from ..db_utils import acquire_with_retry
|
||||||
from ..memory_engine import fq_table
|
from ..memory_engine import fq_table
|
||||||
from .tags import TagsMatch, filter_results_by_tags
|
from .tags import TagGroup, TagsMatch, filter_results_by_tag_groups, filter_results_by_tags
|
||||||
from .types import MPFPTimings, RetrievalResult
|
from .types import MPFPTimings, RetrievalResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -46,6 +46,7 @@ class GraphRetriever(ABC):
|
||||||
adjacency=None, # TypedAdjacency, optional pre-loaded graph
|
adjacency=None, # TypedAdjacency, optional pre-loaded graph
|
||||||
tags: list[str] | None = None, # Visibility scope tags for filtering
|
tags: list[str] | None = None, # Visibility scope tags for filtering
|
||||||
tags_match: TagsMatch = "any", # How to match tags: 'any' (OR) or 'all' (AND)
|
tags_match: TagsMatch = "any", # How to match tags: 'any' (OR) or 'all' (AND)
|
||||||
|
tag_groups: list[TagGroup] | None = None, # Compound boolean tag filter groups
|
||||||
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
||||||
"""
|
"""
|
||||||
Retrieve relevant facts via graph traversal.
|
Retrieve relevant facts via graph traversal.
|
||||||
|
|
@ -120,6 +121,7 @@ class BFSGraphRetriever(GraphRetriever):
|
||||||
adjacency=None, # Not used by BFS
|
adjacency=None, # Not used by BFS
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
||||||
"""
|
"""
|
||||||
Retrieve facts using BFS spreading activation.
|
Retrieve facts using BFS spreading activation.
|
||||||
|
|
@ -136,7 +138,14 @@ class BFSGraphRetriever(GraphRetriever):
|
||||||
"""
|
"""
|
||||||
async with acquire_with_retry(pool) as conn:
|
async with acquire_with_retry(pool) as conn:
|
||||||
results = await self._retrieve_with_conn(
|
results = await self._retrieve_with_conn(
|
||||||
conn, query_embedding_str, bank_id, fact_type, budget, tags=tags, tags_match=tags_match
|
conn,
|
||||||
|
query_embedding_str,
|
||||||
|
bank_id,
|
||||||
|
fact_type,
|
||||||
|
budget,
|
||||||
|
tags=tags,
|
||||||
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
)
|
)
|
||||||
return results, None
|
return results, None
|
||||||
|
|
||||||
|
|
@ -149,14 +158,18 @@ class BFSGraphRetriever(GraphRetriever):
|
||||||
budget: int,
|
budget: int,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> list[RetrievalResult]:
|
) -> list[RetrievalResult]:
|
||||||
"""Internal implementation with connection."""
|
"""Internal implementation with connection."""
|
||||||
from .tags import build_tags_where_clause_simple
|
from .tags import build_tag_groups_where_clause, build_tags_where_clause_simple
|
||||||
|
|
||||||
tags_clause = build_tags_where_clause_simple(tags, 6, match=tags_match)
|
tags_clause = build_tags_where_clause_simple(tags, 6, match=tags_match)
|
||||||
|
tag_groups_param_start = 6 + (1 if tags else 0)
|
||||||
|
groups_clause, groups_params, _ = build_tag_groups_where_clause(tag_groups, tag_groups_param_start)
|
||||||
params = [query_embedding_str, bank_id, fact_type, self.entry_point_threshold, self.entry_point_limit]
|
params = [query_embedding_str, bank_id, fact_type, self.entry_point_threshold, self.entry_point_limit]
|
||||||
if tags:
|
if tags:
|
||||||
params.append(tags)
|
params.append(tags)
|
||||||
|
params.extend(groups_params)
|
||||||
|
|
||||||
# Step 1: Find entry points
|
# Step 1: Find entry points
|
||||||
entry_points = await conn.fetch(
|
entry_points = await conn.fetch(
|
||||||
|
|
@ -170,6 +183,7 @@ class BFSGraphRetriever(GraphRetriever):
|
||||||
AND fact_type = $3
|
AND fact_type = $3
|
||||||
AND (1 - (embedding <=> $1::vector)) >= $4
|
AND (1 - (embedding <=> $1::vector)) >= $4
|
||||||
{tags_clause}
|
{tags_clause}
|
||||||
|
{groups_clause}
|
||||||
ORDER BY embedding <=> $1::vector
|
ORDER BY embedding <=> $1::vector
|
||||||
LIMIT $5
|
LIMIT $5
|
||||||
""",
|
""",
|
||||||
|
|
@ -261,4 +275,8 @@ class BFSGraphRetriever(GraphRetriever):
|
||||||
if tags:
|
if tags:
|
||||||
results = filter_results_by_tags(results, tags, match=tags_match)
|
results = filter_results_by_tags(results, tags, match=tags_match)
|
||||||
|
|
||||||
|
# Apply compound tag group filtering (post-traversal)
|
||||||
|
if tag_groups:
|
||||||
|
results = filter_results_by_tag_groups(results, tag_groups)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import time
|
||||||
from ..db_utils import acquire_with_retry
|
from ..db_utils import acquire_with_retry
|
||||||
from ..memory_engine import fq_table
|
from ..memory_engine import fq_table
|
||||||
from .graph_retrieval import GraphRetriever
|
from .graph_retrieval import GraphRetriever
|
||||||
from .tags import TagsMatch, filter_results_by_tags
|
from .tags import TagGroup, TagsMatch, filter_results_by_tag_groups, filter_results_by_tags
|
||||||
from .types import MPFPTimings, RetrievalResult
|
from .types import MPFPTimings, RetrievalResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -43,14 +43,18 @@ async def _find_semantic_seeds(
|
||||||
threshold: float = 0.3,
|
threshold: float = 0.3,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> list[RetrievalResult]:
|
) -> list[RetrievalResult]:
|
||||||
"""Find semantic seeds via embedding search."""
|
"""Find semantic seeds via embedding search."""
|
||||||
from .tags import build_tags_where_clause_simple
|
from .tags import build_tag_groups_where_clause, build_tags_where_clause_simple
|
||||||
|
|
||||||
tags_clause = build_tags_where_clause_simple(tags, 6, match=tags_match)
|
tags_clause = build_tags_where_clause_simple(tags, 6, match=tags_match)
|
||||||
|
tag_groups_param_start = 6 + (1 if tags else 0)
|
||||||
|
groups_clause, groups_params, _ = build_tag_groups_where_clause(tag_groups, tag_groups_param_start)
|
||||||
params = [query_embedding_str, bank_id, fact_type, threshold, limit]
|
params = [query_embedding_str, bank_id, fact_type, threshold, limit]
|
||||||
if tags:
|
if tags:
|
||||||
params.append(tags)
|
params.append(tags)
|
||||||
|
params.extend(groups_params)
|
||||||
|
|
||||||
rows = await conn.fetch(
|
rows = await conn.fetch(
|
||||||
f"""
|
f"""
|
||||||
|
|
@ -63,6 +67,7 @@ async def _find_semantic_seeds(
|
||||||
AND fact_type = $3
|
AND fact_type = $3
|
||||||
AND (1 - (embedding <=> $1::vector)) >= $4
|
AND (1 - (embedding <=> $1::vector)) >= $4
|
||||||
{tags_clause}
|
{tags_clause}
|
||||||
|
{groups_clause}
|
||||||
ORDER BY embedding <=> $1::vector
|
ORDER BY embedding <=> $1::vector
|
||||||
LIMIT $5
|
LIMIT $5
|
||||||
""",
|
""",
|
||||||
|
|
@ -110,6 +115,7 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||||
adjacency=None,
|
adjacency=None,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
||||||
"""
|
"""
|
||||||
Retrieve facts by expanding links from seeds.
|
Retrieve facts by expanding links from seeds.
|
||||||
|
|
@ -147,6 +153,7 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||||
threshold=0.3,
|
threshold=0.3,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
)
|
)
|
||||||
timings.seeds_time = time.time() - seeds_start
|
timings.seeds_time = time.time() - seeds_start
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
@ -221,6 +228,9 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||||
if tags:
|
if tags:
|
||||||
results = filter_results_by_tags(results, tags, match=tags_match)
|
results = filter_results_by_tags(results, tags, match=tags_match)
|
||||||
|
|
||||||
|
if tag_groups:
|
||||||
|
results = filter_results_by_tag_groups(results, tag_groups)
|
||||||
|
|
||||||
timings.result_count = len(results)
|
timings.result_count = len(results)
|
||||||
timings.traverse = time.time() - start_time
|
timings.traverse = time.time() - start_time
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from dataclasses import dataclass, field
|
||||||
from ..db_utils import acquire_with_retry
|
from ..db_utils import acquire_with_retry
|
||||||
from ..memory_engine import fq_table
|
from ..memory_engine import fq_table
|
||||||
from .graph_retrieval import GraphRetriever
|
from .graph_retrieval import GraphRetriever
|
||||||
from .tags import TagsMatch
|
from .tags import TagGroup, TagsMatch
|
||||||
from .types import MPFPTimings, RetrievalResult
|
from .types import MPFPTimings, RetrievalResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -506,6 +506,7 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||||
adjacency=None, # Ignored - kept for interface compatibility
|
adjacency=None, # Ignored - kept for interface compatibility
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
||||||
"""
|
"""
|
||||||
Retrieve facts using MPFP algorithm with lazy edge loading.
|
Retrieve facts using MPFP algorithm with lazy edge loading.
|
||||||
|
|
@ -537,7 +538,13 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||||
if not semantic_seed_nodes:
|
if not semantic_seed_nodes:
|
||||||
seeds_start = time.time()
|
seeds_start = time.time()
|
||||||
semantic_seed_nodes = await self._find_semantic_seeds(
|
semantic_seed_nodes = await self._find_semantic_seeds(
|
||||||
pool, query_embedding_str, bank_id, fact_type, tags=tags, tags_match=tags_match
|
pool,
|
||||||
|
query_embedding_str,
|
||||||
|
bank_id,
|
||||||
|
fact_type,
|
||||||
|
tags=tags,
|
||||||
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
)
|
)
|
||||||
timings.seeds_time = time.time() - seeds_start
|
timings.seeds_time = time.time() - seeds_start
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
@ -616,6 +623,12 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||||
|
|
||||||
results = filter_results_by_tags(results, tags, match=tags_match)
|
results = filter_results_by_tags(results, tags, match=tags_match)
|
||||||
|
|
||||||
|
# Apply compound tag group filtering (post-traversal)
|
||||||
|
if tag_groups:
|
||||||
|
from .tags import filter_results_by_tag_groups
|
||||||
|
|
||||||
|
results = filter_results_by_tag_groups(results, tag_groups)
|
||||||
|
|
||||||
timings.result_count = len(results)
|
timings.result_count = len(results)
|
||||||
|
|
||||||
# Add activation scores from fusion
|
# Add activation scores from fusion
|
||||||
|
|
@ -656,14 +669,18 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||||
threshold: float = 0.3,
|
threshold: float = 0.3,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> list[SeedNode]:
|
) -> list[SeedNode]:
|
||||||
"""Fallback: find semantic seeds via embedding search."""
|
"""Fallback: find semantic seeds via embedding search."""
|
||||||
from .tags import build_tags_where_clause_simple
|
from .tags import build_tag_groups_where_clause, build_tags_where_clause_simple
|
||||||
|
|
||||||
tags_clause = build_tags_where_clause_simple(tags, 6, match=tags_match)
|
tags_clause = build_tags_where_clause_simple(tags, 6, match=tags_match)
|
||||||
|
tag_groups_param_start = 6 + (1 if tags else 0)
|
||||||
|
groups_clause, groups_params, _ = build_tag_groups_where_clause(tag_groups, tag_groups_param_start)
|
||||||
params = [query_embedding_str, bank_id, fact_type, threshold, limit]
|
params = [query_embedding_str, bank_id, fact_type, threshold, limit]
|
||||||
if tags:
|
if tags:
|
||||||
params.append(tags)
|
params.append(tags)
|
||||||
|
params.extend(groups_params)
|
||||||
|
|
||||||
async with acquire_with_retry(pool) as conn:
|
async with acquire_with_retry(pool) as conn:
|
||||||
rows = await conn.fetch(
|
rows = await conn.fetch(
|
||||||
|
|
@ -675,6 +692,7 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||||
AND fact_type = $3
|
AND fact_type = $3
|
||||||
AND (1 - (embedding <=> $1::vector)) >= $4
|
AND (1 - (embedding <=> $1::vector)) >= $4
|
||||||
{tags_clause}
|
{tags_clause}
|
||||||
|
{groups_clause}
|
||||||
ORDER BY embedding <=> $1::vector
|
ORDER BY embedding <=> $1::vector
|
||||||
LIMIT $5
|
LIMIT $5
|
||||||
""",
|
""",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from ..memory_engine import fq_table
|
||||||
from .graph_retrieval import BFSGraphRetriever, GraphRetriever
|
from .graph_retrieval import BFSGraphRetriever, GraphRetriever
|
||||||
from .link_expansion_retrieval import LinkExpansionRetriever
|
from .link_expansion_retrieval import LinkExpansionRetriever
|
||||||
from .mpfp_retrieval import MPFPGraphRetriever
|
from .mpfp_retrieval import MPFPGraphRetriever
|
||||||
from .tags import TagsMatch, build_tags_where_clause_simple
|
from .tags import TagGroup, TagsMatch, build_tag_groups_where_clause, build_tags_where_clause_simple
|
||||||
from .types import MPFPTimings, RetrievalResult
|
from .types import MPFPTimings, RetrievalResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -94,6 +94,7 @@ async def retrieve_semantic_bm25_combined(
|
||||||
limit: int,
|
limit: int,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> dict[str, tuple[list[RetrievalResult], list[RetrievalResult]]]:
|
) -> dict[str, tuple[list[RetrievalResult], list[RetrievalResult]]]:
|
||||||
"""
|
"""
|
||||||
Combined semantic + BM25 retrieval for multiple fact types in a single query.
|
Combined semantic + BM25 retrieval for multiple fact types in a single query.
|
||||||
|
|
@ -150,9 +151,14 @@ async def retrieve_semantic_bm25_combined(
|
||||||
# $3 = limit (BM25 LIMIT; semantic uses inlined hnsw_fetch literal)
|
# $3 = limit (BM25 LIMIT; semantic uses inlined hnsw_fetch literal)
|
||||||
# $4 = bm25_text (only when tokens present)
|
# $4 = bm25_text (only when tokens present)
|
||||||
# $N = tags (N=4 when no tokens, N=5 when tokens present)
|
# $N = tags (N=4 when no tokens, N=5 when tokens present)
|
||||||
|
# $M+ = tag_groups params (one per leaf, starting after tags param)
|
||||||
tags_param_idx = 5 if tokens else 4
|
tags_param_idx = 5 if tokens else 4
|
||||||
tags_clause = build_tags_where_clause_simple(tags, tags_param_idx, match=tags_match)
|
tags_clause = build_tags_where_clause_simple(tags, tags_param_idx, match=tags_match)
|
||||||
|
|
||||||
|
# tag_groups params start immediately after the tags param slot
|
||||||
|
tag_groups_param_start = tags_param_idx + (1 if tags else 0)
|
||||||
|
groups_clause, groups_params, _ = build_tag_groups_where_clause(tag_groups, tag_groups_param_start)
|
||||||
|
|
||||||
# --- Semantic UNION ALL arms (one per fact_type) ---
|
# --- Semantic UNION ALL arms (one per fact_type) ---
|
||||||
# Each arm has its own ORDER BY embedding <=> $1 LIMIT {hnsw_fetch}, which
|
# Each arm has its own ORDER BY embedding <=> $1 LIMIT {hnsw_fetch}, which
|
||||||
# lets the planner use the partial HNSW index for that fact_type.
|
# lets the planner use the partial HNSW index for that fact_type.
|
||||||
|
|
@ -169,6 +175,7 @@ async def retrieve_semantic_bm25_combined(
|
||||||
f" AND embedding IS NOT NULL"
|
f" AND embedding IS NOT NULL"
|
||||||
f" AND (1 - (embedding <=> $1::vector)) >= 0.3"
|
f" AND (1 - (embedding <=> $1::vector)) >= 0.3"
|
||||||
f" {tags_clause}"
|
f" {tags_clause}"
|
||||||
|
f" {groups_clause}"
|
||||||
f" ORDER BY embedding <=> $1::vector"
|
f" ORDER BY embedding <=> $1::vector"
|
||||||
f" LIMIT {hnsw_fetch})"
|
f" LIMIT {hnsw_fetch})"
|
||||||
)
|
)
|
||||||
|
|
@ -208,6 +215,7 @@ async def retrieve_semantic_bm25_combined(
|
||||||
f" AND fact_type = '{ft}'"
|
f" AND fact_type = '{ft}'"
|
||||||
f" {bm25_where_filter}"
|
f" {bm25_where_filter}"
|
||||||
f" {tags_clause}"
|
f" {tags_clause}"
|
||||||
|
f" {groups_clause}"
|
||||||
f" ORDER BY {bm25_order_by}"
|
f" ORDER BY {bm25_order_by}"
|
||||||
f" LIMIT $3)"
|
f" LIMIT $3)"
|
||||||
)
|
)
|
||||||
|
|
@ -219,6 +227,7 @@ async def retrieve_semantic_bm25_combined(
|
||||||
params.append(bm25_text_param)
|
params.append(bm25_text_param)
|
||||||
if tags:
|
if tags:
|
||||||
params.append(tags)
|
params.append(tags)
|
||||||
|
params.extend(groups_params)
|
||||||
|
|
||||||
rows = await conn.fetch(query, *params)
|
rows = await conn.fetch(query, *params)
|
||||||
|
|
||||||
|
|
@ -251,6 +260,7 @@ async def retrieve_temporal_combined(
|
||||||
semantic_threshold: float = 0.1,
|
semantic_threshold: float = 0.1,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> dict[str, list[RetrievalResult]]:
|
) -> dict[str, list[RetrievalResult]]:
|
||||||
"""
|
"""
|
||||||
Temporal retrieval for multiple fact types in a single query.
|
Temporal retrieval for multiple fact types in a single query.
|
||||||
|
|
@ -280,10 +290,14 @@ async def retrieve_temporal_combined(
|
||||||
end_date = end_date.replace(tzinfo=UTC)
|
end_date = end_date.replace(tzinfo=UTC)
|
||||||
|
|
||||||
# Build tags clause
|
# Build tags clause
|
||||||
|
# Entry point query: fixed params are $1-$6, tags at $7
|
||||||
tags_clause = build_tags_where_clause_simple(tags, 7, match=tags_match)
|
tags_clause = build_tags_where_clause_simple(tags, 7, match=tags_match)
|
||||||
params = [query_emb_str, bank_id, fact_types, start_date, end_date, semantic_threshold]
|
tag_groups_param_start = 7 + (1 if tags else 0)
|
||||||
|
groups_clause, groups_params, _ = build_tag_groups_where_clause(tag_groups, tag_groups_param_start)
|
||||||
|
params: list = [query_emb_str, bank_id, fact_types, start_date, end_date, semantic_threshold]
|
||||||
if tags:
|
if tags:
|
||||||
params.append(tags)
|
params.append(tags)
|
||||||
|
params.extend(groups_params)
|
||||||
|
|
||||||
# Two-phase entry point query:
|
# Two-phase entry point query:
|
||||||
# Phase 1 (date_ranked): rank by date only — no embedding computation — for all units in
|
# Phase 1 (date_ranked): rank by date only — no embedding computation — for all units in
|
||||||
|
|
@ -314,6 +328,7 @@ async def retrieve_temporal_combined(
|
||||||
(occurred_end IS NOT NULL AND occurred_end BETWEEN $4 AND $5)
|
(occurred_end IS NOT NULL AND occurred_end BETWEEN $4 AND $5)
|
||||||
)
|
)
|
||||||
{tags_clause}
|
{tags_clause}
|
||||||
|
{groups_clause}
|
||||||
),
|
),
|
||||||
sim_ranked AS (
|
sim_ranked AS (
|
||||||
SELECT mu.id, mu.text, mu.context, mu.event_date, mu.occurred_start, mu.occurred_end, mu.mentioned_at, mu.fact_type, mu.document_id, mu.chunk_id, mu.tags,
|
SELECT mu.id, mu.text, mu.context, mu.event_date, mu.occurred_start, mu.occurred_end, mu.mentioned_at, mu.fact_type, mu.document_id, mu.chunk_id, mu.tags,
|
||||||
|
|
@ -400,16 +415,21 @@ async def retrieve_temporal_combined(
|
||||||
|
|
||||||
# Build tags clause for spreading (use param 7 since 1-6 are used)
|
# Build tags clause for spreading (use param 7 since 1-6 are used)
|
||||||
spreading_tags_clause = build_tags_where_clause_simple(tags, 7, table_alias="mu.", match=tags_match)
|
spreading_tags_clause = build_tags_where_clause_simple(tags, 7, table_alias="mu.", match=tags_match)
|
||||||
|
spreading_groups_param_start = 7 + (1 if tags else 0)
|
||||||
|
spreading_groups_clause, spreading_groups_params, _ = build_tag_groups_where_clause(
|
||||||
|
tag_groups, spreading_groups_param_start, table_alias="mu."
|
||||||
|
)
|
||||||
|
|
||||||
while frontier and budget_remaining > 0 and iteration < max_iterations:
|
while frontier and budget_remaining > 0 and iteration < max_iterations:
|
||||||
iteration += 1
|
iteration += 1
|
||||||
batch_ids = frontier[:batch_size]
|
batch_ids = frontier[:batch_size]
|
||||||
frontier = frontier[batch_size:]
|
frontier = frontier[batch_size:]
|
||||||
|
|
||||||
# $1=query_emb, $2=batch_ids, $3=fact_type, $4=threshold, $5=per_source_limit, $6=bank_id, $7=tags
|
# $1=query_emb, $2=batch_ids, $3=fact_type, $4=threshold, $5=per_source_limit, $6=bank_id, $7=tags, $M+=tag_groups
|
||||||
spreading_params = [query_emb_str, batch_ids, ft, semantic_threshold, per_source_limit, bank_id]
|
spreading_params = [query_emb_str, batch_ids, ft, semantic_threshold, per_source_limit, bank_id]
|
||||||
if tags:
|
if tags:
|
||||||
spreading_params.append(tags)
|
spreading_params.append(tags)
|
||||||
|
spreading_params.extend(spreading_groups_params)
|
||||||
|
|
||||||
# LATERAL join: for each source node, fetch top-K neighbors by weight using
|
# LATERAL join: for each source node, fetch top-K neighbors by weight using
|
||||||
# the existing idx_memory_links_from_type_weight index with early-exit semantics.
|
# the existing idx_memory_links_from_type_weight index with early-exit semantics.
|
||||||
|
|
@ -436,6 +456,7 @@ async def retrieve_temporal_combined(
|
||||||
AND mu.embedding IS NOT NULL
|
AND mu.embedding IS NOT NULL
|
||||||
AND (1 - (mu.embedding <=> $1::vector)) >= $4
|
AND (1 - (mu.embedding <=> $1::vector)) >= $4
|
||||||
{spreading_tags_clause}
|
{spreading_tags_clause}
|
||||||
|
{spreading_groups_clause}
|
||||||
""",
|
""",
|
||||||
*spreading_params,
|
*spreading_params,
|
||||||
)
|
)
|
||||||
|
|
@ -509,6 +530,7 @@ async def retrieve_all_fact_types_parallel(
|
||||||
graph_retriever: GraphRetriever | None = None,
|
graph_retriever: GraphRetriever | None = None,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
tags_match: TagsMatch = "any",
|
tags_match: TagsMatch = "any",
|
||||||
|
tag_groups: list[TagGroup] | None = None,
|
||||||
) -> MultiFactTypeRetrievalResult:
|
) -> MultiFactTypeRetrievalResult:
|
||||||
"""
|
"""
|
||||||
Optimized retrieval for multiple fact types using batched queries.
|
Optimized retrieval for multiple fact types using batched queries.
|
||||||
|
|
@ -566,6 +588,7 @@ async def retrieve_all_fact_types_parallel(
|
||||||
thinking_budget,
|
thinking_budget,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
)
|
)
|
||||||
semantic_bm25_time = time.time() - semantic_bm25_start
|
semantic_bm25_time = time.time() - semantic_bm25_start
|
||||||
|
|
||||||
|
|
@ -584,6 +607,7 @@ async def retrieve_all_fact_types_parallel(
|
||||||
semantic_threshold=0.1,
|
semantic_threshold=0.1,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
)
|
)
|
||||||
temporal_time = time.time() - temporal_start
|
temporal_time = time.time() - temporal_start
|
||||||
|
|
||||||
|
|
@ -604,6 +628,7 @@ async def retrieve_all_fact_types_parallel(
|
||||||
temporal_seeds=None,
|
temporal_seeds=None,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tags_match=tags_match,
|
tags_match=tags_match,
|
||||||
|
tag_groups=tag_groups,
|
||||||
)
|
)
|
||||||
return ft, results, time.time() - graph_start, mpfp_timing
|
return ft, results, time.time() - graph_start, mpfp_timing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ OR matching (any/any_strict): Memory matches if ANY of its tags overlap with req
|
||||||
AND matching (all/all_strict): Memory matches if ALL request tags are present in its tags
|
AND matching (all/all_strict): Memory matches if ALL request tags are present in its tags
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Literal
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
TagsMatch = Literal["any", "all", "any_strict", "all_strict"]
|
TagsMatch = Literal["any", "all", "any_strict", "all_strict"]
|
||||||
|
|
||||||
|
|
@ -170,3 +174,217 @@ def filter_results_by_tags(
|
||||||
filtered.append(result)
|
filtered.append(result)
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Compound tag group models (recursive boolean expressions)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TagGroupLeaf(BaseModel):
|
||||||
|
"""A leaf tag filter: matches memories by tag list and match mode."""
|
||||||
|
|
||||||
|
tags: list[str]
|
||||||
|
match: TagsMatch = "any_strict"
|
||||||
|
|
||||||
|
|
||||||
|
class TagGroupAnd(BaseModel):
|
||||||
|
"""Compound AND group: all child filters must match."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
filters: list[TagGroup] = Field(alias="and")
|
||||||
|
|
||||||
|
|
||||||
|
class TagGroupOr(BaseModel):
|
||||||
|
"""Compound OR group: at least one child filter must match."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
filters: list[TagGroup] = Field(alias="or")
|
||||||
|
|
||||||
|
|
||||||
|
class TagGroupNot(BaseModel):
|
||||||
|
"""Compound NOT group: child filter must NOT match."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
filter: TagGroup = Field(alias="not")
|
||||||
|
|
||||||
|
|
||||||
|
# TagGroup is a discriminated union; Pydantic will try left-to-right.
|
||||||
|
# TagGroupLeaf is identified by the presence of 'tags'.
|
||||||
|
# TagGroupAnd / TagGroupOr / TagGroupNot are compound (no 'tags' key).
|
||||||
|
TagGroup = Annotated[
|
||||||
|
TagGroupLeaf | TagGroupAnd | TagGroupOr | TagGroupNot,
|
||||||
|
Field(union_mode="left_to_right"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Rebuild forward-reference models so recursive TagGroup is resolved.
|
||||||
|
TagGroupAnd.model_rebuild()
|
||||||
|
TagGroupOr.model_rebuild()
|
||||||
|
TagGroupNot.model_rebuild()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SQL builder for compound tag groups
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _build_group_clause(
|
||||||
|
group: TagGroup,
|
||||||
|
param_offset: int,
|
||||||
|
table_alias: str,
|
||||||
|
) -> tuple[str, list, int]:
|
||||||
|
"""
|
||||||
|
Recursively build an inner SQL clause (no leading AND/OR) for a single TagGroup.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(inner_clause, params, next_param_offset)
|
||||||
|
"""
|
||||||
|
if isinstance(group, TagGroupLeaf):
|
||||||
|
column = f"{table_alias}tags" if table_alias else "tags"
|
||||||
|
operator, include_untagged = _parse_tags_match(group.match)
|
||||||
|
if include_untagged:
|
||||||
|
clause = f"({column} IS NULL OR {column} = '{{}}' OR {column} {operator} ${param_offset})"
|
||||||
|
else:
|
||||||
|
clause = f"({column} IS NOT NULL AND {column} != '{{}}' AND {column} {operator} ${param_offset})"
|
||||||
|
return clause, [group.tags], param_offset + 1
|
||||||
|
|
||||||
|
elif isinstance(group, TagGroupAnd):
|
||||||
|
parts = []
|
||||||
|
params: list = []
|
||||||
|
offset = param_offset
|
||||||
|
for child in group.filters:
|
||||||
|
child_clause, child_params, offset = _build_group_clause(child, offset, table_alias)
|
||||||
|
parts.append(child_clause)
|
||||||
|
params.extend(child_params)
|
||||||
|
inner = " AND ".join(parts)
|
||||||
|
return f"({inner})", params, offset
|
||||||
|
|
||||||
|
elif isinstance(group, TagGroupOr):
|
||||||
|
parts = []
|
||||||
|
params = []
|
||||||
|
offset = param_offset
|
||||||
|
for child in group.filters:
|
||||||
|
child_clause, child_params, offset = _build_group_clause(child, offset, table_alias)
|
||||||
|
parts.append(child_clause)
|
||||||
|
params.extend(child_params)
|
||||||
|
inner = " OR ".join(parts)
|
||||||
|
return f"({inner})", params, offset
|
||||||
|
|
||||||
|
elif isinstance(group, TagGroupNot):
|
||||||
|
child_clause, child_params, next_offset = _build_group_clause(group.filter, param_offset, table_alias)
|
||||||
|
return f"NOT {child_clause}", child_params, next_offset
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Should never happen with proper Pydantic validation
|
||||||
|
return "", [], param_offset
|
||||||
|
|
||||||
|
|
||||||
|
def build_tag_groups_where_clause(
|
||||||
|
tag_groups: list[TagGroup] | None,
|
||||||
|
param_offset: int,
|
||||||
|
table_alias: str = "",
|
||||||
|
) -> tuple[str, list, int]:
|
||||||
|
"""
|
||||||
|
Build a SQL WHERE clause for compound tag group filtering.
|
||||||
|
|
||||||
|
Top-level groups are AND-ed together. Each group is a recursive boolean
|
||||||
|
expression (leaf, and, or, not).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_groups: List of TagGroup objects. If None or empty, returns empty clause.
|
||||||
|
param_offset: Starting parameter number for SQL placeholders.
|
||||||
|
table_alias: Optional table alias prefix (e.g., "mu." for "memory_units mu").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (sql_clause, params, next_param_offset):
|
||||||
|
- sql_clause: SQL WHERE clause string starting with "AND" (or empty string)
|
||||||
|
- params: List of parameter values to bind (one per leaf node)
|
||||||
|
- next_param_offset: Next available parameter number
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> groups = [TagGroupLeaf(tags=["user:alice"], match="all_strict")]
|
||||||
|
>>> clause, params, next_offset = build_tag_groups_where_clause(groups, 3)
|
||||||
|
>>> print(clause) # "AND (tags IS NOT NULL AND tags != '{}' AND tags @> $3)"
|
||||||
|
"""
|
||||||
|
if not tag_groups:
|
||||||
|
return "", [], param_offset
|
||||||
|
|
||||||
|
all_params: list = []
|
||||||
|
all_clauses: list[str] = []
|
||||||
|
offset = param_offset
|
||||||
|
|
||||||
|
for group in tag_groups:
|
||||||
|
inner_clause, group_params, offset = _build_group_clause(group, offset, table_alias)
|
||||||
|
all_clauses.append(inner_clause)
|
||||||
|
all_params.extend(group_params)
|
||||||
|
|
||||||
|
combined = " AND ".join(all_clauses)
|
||||||
|
return f"AND {combined}", all_params, offset
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Python-side filter for compound tag groups (post-retrieval filtering)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _match_group(result: object, group: TagGroup) -> bool:
|
||||||
|
"""
|
||||||
|
Recursively evaluate a TagGroup against a retrieval result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: Any object with a 'tags' attribute (list[str] or None).
|
||||||
|
group: The TagGroup to evaluate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the result matches the group, False otherwise.
|
||||||
|
"""
|
||||||
|
if isinstance(group, TagGroupLeaf):
|
||||||
|
result_tags = getattr(result, "tags", None)
|
||||||
|
is_untagged = result_tags is None or len(result_tags) == 0
|
||||||
|
_, include_untagged = _parse_tags_match(group.match)
|
||||||
|
is_any_match = group.match in ("any", "any_strict")
|
||||||
|
tags_set = set(group.tags)
|
||||||
|
|
||||||
|
if is_untagged:
|
||||||
|
return include_untagged
|
||||||
|
else:
|
||||||
|
result_tags_set = set(result_tags)
|
||||||
|
if is_any_match:
|
||||||
|
return bool(result_tags_set & tags_set)
|
||||||
|
else:
|
||||||
|
return tags_set <= result_tags_set
|
||||||
|
|
||||||
|
elif isinstance(group, TagGroupAnd):
|
||||||
|
return all(_match_group(result, child) for child in group.filters)
|
||||||
|
|
||||||
|
elif isinstance(group, TagGroupOr):
|
||||||
|
return any(_match_group(result, child) for child in group.filters)
|
||||||
|
|
||||||
|
elif isinstance(group, TagGroupNot):
|
||||||
|
return not _match_group(result, group.filter)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def filter_results_by_tag_groups(
|
||||||
|
results: list,
|
||||||
|
tag_groups: list[TagGroup] | None,
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Filter retrieval results by compound tag groups in Python (for post-processing).
|
||||||
|
|
||||||
|
Used when SQL filtering isn't possible (e.g., graph traversal results).
|
||||||
|
Top-level groups are AND-ed together.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: List of RetrievalResult objects with a 'tags' attribute.
|
||||||
|
tag_groups: List of TagGroup objects. If None or empty, returns all results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of results where ALL top-level groups match.
|
||||||
|
"""
|
||||||
|
if not tag_groups:
|
||||||
|
return results
|
||||||
|
|
||||||
|
return [r for r in results if all(_match_group(r, group) for group in tag_groups)]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,16 @@ import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from hindsight_api.api import create_app
|
from hindsight_api.api import create_app
|
||||||
from hindsight_api.engine.search.tags import build_tags_where_clause_simple, filter_results_by_tags
|
from hindsight_api.engine.search.tags import (
|
||||||
|
TagGroupAnd,
|
||||||
|
TagGroupLeaf,
|
||||||
|
TagGroupNot,
|
||||||
|
TagGroupOr,
|
||||||
|
build_tag_groups_where_clause,
|
||||||
|
build_tags_where_clause_simple,
|
||||||
|
filter_results_by_tag_groups,
|
||||||
|
filter_results_by_tags,
|
||||||
|
)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Unit Tests for tags SQL builder
|
# Unit Tests for tags SQL builder
|
||||||
|
|
@ -263,6 +272,327 @@ class TestFilterResultsByTags:
|
||||||
assert missing_session not in filtered
|
assert missing_session not in filtered
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Unit Tests for build_tag_groups_where_clause (SQL builder)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildTagGroupsWhereClause:
|
||||||
|
"""Unit tests for the compound tag group SQL builder."""
|
||||||
|
|
||||||
|
def test_none_returns_empty(self):
|
||||||
|
"""None tag_groups returns empty clause."""
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(None, 3)
|
||||||
|
assert clause == ""
|
||||||
|
assert params == []
|
||||||
|
assert next_offset == 3
|
||||||
|
|
||||||
|
def test_empty_list_returns_empty(self):
|
||||||
|
"""Empty tag_groups list returns empty clause."""
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause([], 3)
|
||||||
|
assert clause == ""
|
||||||
|
assert params == []
|
||||||
|
assert next_offset == 3
|
||||||
|
|
||||||
|
def test_single_leaf_any_strict(self):
|
||||||
|
"""Single any_strict leaf generates correct SQL."""
|
||||||
|
groups = [TagGroupLeaf(tags=["step:5", "step:8"], match="any_strict")]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 3)
|
||||||
|
assert clause.startswith("AND ")
|
||||||
|
assert "$3" in clause
|
||||||
|
assert "IS NOT NULL" in clause
|
||||||
|
assert "!= '{}'" in clause
|
||||||
|
assert "&&" in clause
|
||||||
|
assert params == [["step:5", "step:8"]]
|
||||||
|
assert next_offset == 4
|
||||||
|
|
||||||
|
def test_single_leaf_all_strict(self):
|
||||||
|
"""Single all_strict leaf generates @> operator."""
|
||||||
|
groups = [TagGroupLeaf(tags=["user:alice"], match="all_strict")]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 1)
|
||||||
|
assert "@>" in clause
|
||||||
|
assert "IS NOT NULL" in clause
|
||||||
|
assert params == [["user:alice"]]
|
||||||
|
assert next_offset == 2
|
||||||
|
|
||||||
|
def test_single_leaf_any_includes_untagged(self):
|
||||||
|
"""Single any (non-strict) leaf generates NULL-inclusive clause."""
|
||||||
|
groups = [TagGroupLeaf(tags=["user:alice"], match="any")]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 1)
|
||||||
|
assert "IS NULL" in clause
|
||||||
|
assert "= '{}'" in clause
|
||||||
|
assert "&&" in clause
|
||||||
|
assert params == [["user:alice"]]
|
||||||
|
assert next_offset == 2
|
||||||
|
|
||||||
|
def test_and_of_two_leaves(self):
|
||||||
|
"""AND of two leaves generates AND-joined clause."""
|
||||||
|
groups = [
|
||||||
|
TagGroupAnd.model_validate(
|
||||||
|
{"and": [
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
{"tags": ["user:ep_42"], "match": "all_strict"},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 3)
|
||||||
|
assert "AND" in clause
|
||||||
|
assert "$3" in clause
|
||||||
|
assert "$4" in clause
|
||||||
|
assert len(params) == 2
|
||||||
|
assert params[0] == ["step:5"]
|
||||||
|
assert params[1] == ["user:ep_42"]
|
||||||
|
assert next_offset == 5
|
||||||
|
|
||||||
|
def test_or_of_two_leaves(self):
|
||||||
|
"""OR of two leaves generates OR-joined clause."""
|
||||||
|
groups = [
|
||||||
|
TagGroupOr.model_validate(
|
||||||
|
{"or": [
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
{"tags": ["priority:high"], "match": "all_strict"},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 1)
|
||||||
|
assert "OR" in clause
|
||||||
|
assert "$1" in clause
|
||||||
|
assert "$2" in clause
|
||||||
|
assert len(params) == 2
|
||||||
|
assert next_offset == 3
|
||||||
|
|
||||||
|
def test_not_wraps_with_not(self):
|
||||||
|
"""NOT group wraps child clause with NOT."""
|
||||||
|
groups = [
|
||||||
|
TagGroupNot.model_validate(
|
||||||
|
{"not": {"tags": ["archived"], "match": "any_strict"}}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 2)
|
||||||
|
assert "NOT" in clause
|
||||||
|
assert "$2" in clause
|
||||||
|
assert len(params) == 1
|
||||||
|
assert next_offset == 3
|
||||||
|
|
||||||
|
def test_nested_and_containing_or(self):
|
||||||
|
"""AND containing an OR generates correct nested SQL."""
|
||||||
|
groups = [
|
||||||
|
TagGroupAnd.model_validate(
|
||||||
|
{"and": [
|
||||||
|
{"tags": ["user:alice"], "match": "all_strict"},
|
||||||
|
{"or": [
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
{"tags": ["priority:high"], "match": "all_strict"},
|
||||||
|
]},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 1)
|
||||||
|
assert "AND" in clause
|
||||||
|
assert "OR" in clause
|
||||||
|
assert len(params) == 3
|
||||||
|
assert next_offset == 4
|
||||||
|
|
||||||
|
def test_param_numbering_sequential(self):
|
||||||
|
"""Params are numbered sequentially starting from param_offset."""
|
||||||
|
groups = [
|
||||||
|
TagGroupAnd.model_validate(
|
||||||
|
{"and": [
|
||||||
|
{"tags": ["a"], "match": "any_strict"},
|
||||||
|
{"tags": ["b"], "match": "any_strict"},
|
||||||
|
{"tags": ["c"], "match": "any_strict"},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 5)
|
||||||
|
assert "$5" in clause
|
||||||
|
assert "$6" in clause
|
||||||
|
assert "$7" in clause
|
||||||
|
assert next_offset == 8
|
||||||
|
assert len(params) == 3
|
||||||
|
|
||||||
|
def test_table_alias_applied_to_leaves(self):
|
||||||
|
"""Table alias is prefixed to column name in all leaf clauses."""
|
||||||
|
groups = [TagGroupLeaf(tags=["user:alice"], match="any_strict")]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 1, table_alias="mu.")
|
||||||
|
assert "mu.tags" in clause
|
||||||
|
|
||||||
|
def test_table_alias_propagates_to_nested(self):
|
||||||
|
"""Table alias propagates to nested leaves (each leaf uses the alias)."""
|
||||||
|
groups = [
|
||||||
|
TagGroupAnd.model_validate(
|
||||||
|
{"and": [
|
||||||
|
{"tags": ["a"], "match": "any_strict"},
|
||||||
|
{"tags": ["b"], "match": "any_strict"},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 1, table_alias="mu.")
|
||||||
|
# Each leaf of type any_strict references mu.tags three times (IS NOT NULL, != '{}', &&)
|
||||||
|
# We verify that 'tags' without alias is NOT present, proving the alias is always used
|
||||||
|
assert "mu.tags" in clause
|
||||||
|
# No bare 'tags' keyword without the alias prefix (other than inside the alias itself)
|
||||||
|
import re
|
||||||
|
bare_tags = re.findall(r"(?<!\.)tags", clause)
|
||||||
|
assert len(bare_tags) == 0, f"Found bare 'tags' references without alias: {bare_tags}"
|
||||||
|
|
||||||
|
def test_multiple_top_level_groups_are_anded(self):
|
||||||
|
"""Multiple top-level groups are AND-ed together."""
|
||||||
|
groups = [
|
||||||
|
TagGroupLeaf(tags=["step:5"], match="any_strict"),
|
||||||
|
TagGroupLeaf(tags=["user:ep_42"], match="all_strict"),
|
||||||
|
]
|
||||||
|
clause, params, next_offset = build_tag_groups_where_clause(groups, 1)
|
||||||
|
# Should start with AND and have two param refs joined by AND
|
||||||
|
assert clause.startswith("AND ")
|
||||||
|
assert " AND " in clause[4:] # after the leading "AND "
|
||||||
|
assert "$1" in clause
|
||||||
|
assert "$2" in clause
|
||||||
|
assert len(params) == 2
|
||||||
|
assert next_offset == 3
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Unit Tests for filter_results_by_tag_groups (Python-side)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilterResultsByTagGroups:
|
||||||
|
"""Unit tests for the Python-side compound tag group filter."""
|
||||||
|
|
||||||
|
def test_none_returns_all(self):
|
||||||
|
"""None tag_groups returns all results."""
|
||||||
|
results = [MockResult(["a"]), MockResult(["b"]), MockResult(None)]
|
||||||
|
filtered = filter_results_by_tag_groups(results, None)
|
||||||
|
assert len(filtered) == 3
|
||||||
|
|
||||||
|
def test_empty_list_returns_all(self):
|
||||||
|
"""Empty tag_groups list returns all results."""
|
||||||
|
results = [MockResult(["a"]), MockResult(None)]
|
||||||
|
filtered = filter_results_by_tag_groups(results, [])
|
||||||
|
assert len(filtered) == 2
|
||||||
|
|
||||||
|
def test_single_leaf_any_strict_excludes_untagged(self):
|
||||||
|
"""Single any_strict leaf excludes untagged results."""
|
||||||
|
groups = [TagGroupLeaf(tags=["step:5"], match="any_strict")]
|
||||||
|
results = [MockResult(["step:5"]), MockResult(["step:9"]), MockResult(None)]
|
||||||
|
filtered = filter_results_by_tag_groups(results, groups)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].tags == ["step:5"]
|
||||||
|
|
||||||
|
def test_single_leaf_all_strict_matches_superset(self):
|
||||||
|
"""Single all_strict leaf matches results that contain all tags."""
|
||||||
|
groups = [TagGroupLeaf(tags=["user:alice", "step:5"], match="all_strict")]
|
||||||
|
results = [
|
||||||
|
MockResult(["user:alice", "step:5"]),
|
||||||
|
MockResult(["user:alice", "step:5", "extra"]),
|
||||||
|
MockResult(["user:alice"]),
|
||||||
|
MockResult(None),
|
||||||
|
]
|
||||||
|
filtered = filter_results_by_tag_groups(results, groups)
|
||||||
|
assert len(filtered) == 2
|
||||||
|
|
||||||
|
def test_and_both_conditions_must_match(self):
|
||||||
|
"""AND group: both leaf conditions must match."""
|
||||||
|
groups = [
|
||||||
|
TagGroupAnd.model_validate(
|
||||||
|
{"and": [
|
||||||
|
{"tags": ["user:alice"], "match": "all_strict"},
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
results = [
|
||||||
|
MockResult(["user:alice", "step:5"]), # matches both
|
||||||
|
MockResult(["user:alice"]), # only matches first
|
||||||
|
MockResult(["step:5"]), # only matches second
|
||||||
|
MockResult(None),
|
||||||
|
]
|
||||||
|
filtered = filter_results_by_tag_groups(results, groups)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].tags == ["user:alice", "step:5"]
|
||||||
|
|
||||||
|
def test_or_either_condition_matches(self):
|
||||||
|
"""OR group: either condition matching is sufficient."""
|
||||||
|
groups = [
|
||||||
|
TagGroupOr.model_validate(
|
||||||
|
{"or": [
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
{"tags": ["priority:high"], "match": "all_strict"},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
results = [
|
||||||
|
MockResult(["step:5"]),
|
||||||
|
MockResult(["priority:high"]),
|
||||||
|
MockResult(["step:5", "priority:high"]),
|
||||||
|
MockResult(["other"]),
|
||||||
|
MockResult(None),
|
||||||
|
]
|
||||||
|
filtered = filter_results_by_tag_groups(results, groups)
|
||||||
|
# step:5, priority:high, and step:5+priority:high all match
|
||||||
|
assert len(filtered) == 3
|
||||||
|
|
||||||
|
def test_not_negation(self):
|
||||||
|
"""NOT group: inverts the child match."""
|
||||||
|
groups = [
|
||||||
|
TagGroupNot.model_validate(
|
||||||
|
{"not": {"tags": ["archived"], "match": "any_strict"}}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
results = [
|
||||||
|
MockResult(["archived"]),
|
||||||
|
MockResult(["active"]),
|
||||||
|
MockResult(["archived", "active"]),
|
||||||
|
MockResult(None),
|
||||||
|
]
|
||||||
|
filtered = filter_results_by_tag_groups(results, groups)
|
||||||
|
# "archived" and "archived+active" should be excluded
|
||||||
|
# "active" and None pass (None is untagged, "any_strict" for "archived" would exclude
|
||||||
|
# untagged, so NOT(exclude untagged) = include untagged)
|
||||||
|
tags_in_filtered = [r.tags for r in filtered]
|
||||||
|
assert ["archived"] not in tags_in_filtered
|
||||||
|
assert ["archived", "active"] not in tags_in_filtered
|
||||||
|
|
||||||
|
def test_nested_and_containing_or(self):
|
||||||
|
"""AND containing OR: nested boolean logic works correctly."""
|
||||||
|
groups = [
|
||||||
|
TagGroupAnd.model_validate(
|
||||||
|
{"and": [
|
||||||
|
{"tags": ["user:alice"], "match": "all_strict"},
|
||||||
|
{"or": [
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
{"tags": ["priority:high"], "match": "any_strict"},
|
||||||
|
]},
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
results = [
|
||||||
|
MockResult(["user:alice", "step:5"]), # user:alice AND (step:5 OR ...)
|
||||||
|
MockResult(["user:alice", "priority:high"]), # user:alice AND (... OR priority:high)
|
||||||
|
MockResult(["user:alice"]), # user:alice but neither step nor priority
|
||||||
|
MockResult(["step:5"]), # step:5 but not user:alice
|
||||||
|
MockResult(None),
|
||||||
|
]
|
||||||
|
filtered = filter_results_by_tag_groups(results, groups)
|
||||||
|
assert len(filtered) == 2
|
||||||
|
|
||||||
|
def test_multiple_top_level_groups_are_anded(self):
|
||||||
|
"""Multiple top-level tag groups are AND-ed."""
|
||||||
|
groups = [
|
||||||
|
TagGroupLeaf(tags=["user:alice"], match="all_strict"),
|
||||||
|
TagGroupLeaf(tags=["step:5"], match="any_strict"),
|
||||||
|
]
|
||||||
|
results = [
|
||||||
|
MockResult(["user:alice", "step:5"]), # both match
|
||||||
|
MockResult(["user:alice"]), # only first
|
||||||
|
MockResult(["step:5"]), # only second
|
||||||
|
]
|
||||||
|
filtered = filter_results_by_tag_groups(results, groups)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].tags == ["user:alice", "step:5"]
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Integration Tests for tags in retain/recall/reflect
|
# Integration Tests for tags in retain/recall/reflect
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -956,3 +1286,218 @@ async def test_list_memories_includes_tags(api_client, test_bank_id):
|
||||||
assert set(memory_item["tags"]) == set(tags), (
|
assert set(memory_item["tags"]) == set(tags), (
|
||||||
f"All {len(tags)} tags should be returned, got: {memory_item['tags']}"
|
f"All {len(tags)} tags should be returned, got: {memory_item['tags']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Integration Tests for tag_groups compound filtering
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tag_groups_validation_rejects_both_tags_and_tag_groups(api_client, test_bank_id):
|
||||||
|
"""Passing both tags and tag_groups must be rejected (422)."""
|
||||||
|
response = await api_client.post(
|
||||||
|
f"/v1/default/banks/{test_bank_id}/memories/recall",
|
||||||
|
json={
|
||||||
|
"query": "anything",
|
||||||
|
"tags": ["user:alice"],
|
||||||
|
"tag_groups": [{"tags": ["user:alice"], "match": "any_strict"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 422, (
|
||||||
|
f"Expected 422 when both tags and tag_groups are set, got {response.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tag_groups_leaf_and_filter(api_client):
|
||||||
|
"""
|
||||||
|
Two leaf groups at top level (implicit AND): step filter AND user scope.
|
||||||
|
|
||||||
|
Retain:
|
||||||
|
- Memory A: tags=[step:5, user:alice] ← should match
|
||||||
|
- Memory B: tags=[step:5, user:bob] ← excluded (wrong user)
|
||||||
|
- Memory C: tags=[step:9, user:alice] ← excluded (wrong step)
|
||||||
|
|
||||||
|
tag_groups = [{tags:[step:5], match:any_strict}, {tags:[user:alice], match:all_strict}]
|
||||||
|
Expected: only A.
|
||||||
|
"""
|
||||||
|
bank_id = f"tg_and_{datetime.now().timestamp()}"
|
||||||
|
|
||||||
|
retain = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories",
|
||||||
|
json={
|
||||||
|
"items": [
|
||||||
|
{"content": "Alice completed step 5 of the onboarding process.", "tags": ["step:5", "user:alice"]},
|
||||||
|
{"content": "Bob completed step 5 of the onboarding process.", "tags": ["step:5", "user:bob"]},
|
||||||
|
{"content": "Alice completed step 9 of the onboarding process.", "tags": ["step:9", "user:alice"]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert retain.status_code == 200
|
||||||
|
|
||||||
|
response = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories/recall",
|
||||||
|
json={
|
||||||
|
"query": "onboarding step completion",
|
||||||
|
"budget": "mid",
|
||||||
|
"tag_groups": [
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
{"tags": ["user:alice"], "match": "all_strict"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
texts = [r["text"] for r in response.json()["results"]]
|
||||||
|
|
||||||
|
assert any("Alice" in t and "step 5" in t for t in texts), "Should find Alice step:5 memory"
|
||||||
|
assert not any("Bob" in t for t in texts), "Should NOT find Bob (wrong user)"
|
||||||
|
assert not any("step 9" in t for t in texts), "Should NOT find step 9 (wrong step)"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tag_groups_or_compound(api_client):
|
||||||
|
"""
|
||||||
|
OR compound: match user:alice OR user:bob, but not user:carol.
|
||||||
|
|
||||||
|
Retain:
|
||||||
|
- Memory A: tags=[user:alice]
|
||||||
|
- Memory B: tags=[user:bob]
|
||||||
|
- Memory C: tags=[user:carol]
|
||||||
|
|
||||||
|
tag_groups = [{or: [{tags:[user:alice]}, {tags:[user:bob]}]}]
|
||||||
|
Expected: A and B, not C.
|
||||||
|
"""
|
||||||
|
bank_id = f"tg_or_{datetime.now().timestamp()}"
|
||||||
|
|
||||||
|
retain = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories",
|
||||||
|
json={
|
||||||
|
"items": [
|
||||||
|
{"content": "Alice is a machine learning engineer.", "tags": ["user:alice"]},
|
||||||
|
{"content": "Bob is a backend software engineer.", "tags": ["user:bob"]},
|
||||||
|
{"content": "Carol is a product manager.", "tags": ["user:carol"]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert retain.status_code == 200
|
||||||
|
|
||||||
|
response = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories/recall",
|
||||||
|
json={
|
||||||
|
"query": "what are the engineers working on",
|
||||||
|
"budget": "mid",
|
||||||
|
"tag_groups": [
|
||||||
|
{"or": [
|
||||||
|
{"tags": ["user:alice"], "match": "any_strict"},
|
||||||
|
{"tags": ["user:bob"], "match": "any_strict"},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
texts = [r["text"] for r in response.json()["results"]]
|
||||||
|
|
||||||
|
assert any("Alice" in t for t in texts), "Should find Alice (in OR)"
|
||||||
|
assert any("Bob" in t for t in texts), "Should find Bob (in OR)"
|
||||||
|
assert not any("Carol" in t for t in texts), "Should NOT find Carol (not in OR)"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tag_groups_not_compound(api_client):
|
||||||
|
"""
|
||||||
|
NOT compound: user:alice AND NOT archived.
|
||||||
|
|
||||||
|
Retain:
|
||||||
|
- Memory A: tags=[user:alice] ← should match
|
||||||
|
- Memory B: tags=[user:alice, archived] ← excluded (archived)
|
||||||
|
- Memory C: tags=[user:bob] ← excluded (wrong user)
|
||||||
|
|
||||||
|
tag_groups = [{tags:[user:alice], match:any_strict}, {not: {tags:[archived], match:any_strict}}]
|
||||||
|
Expected: only A.
|
||||||
|
"""
|
||||||
|
bank_id = f"tg_not_{datetime.now().timestamp()}"
|
||||||
|
|
||||||
|
retain = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories",
|
||||||
|
json={
|
||||||
|
"items": [
|
||||||
|
{"content": "Alice joined the data science team this quarter.", "tags": ["user:alice"]},
|
||||||
|
{"content": "Alice left the previous analytics project last year.", "tags": ["user:alice", "archived"]},
|
||||||
|
{"content": "Bob joined the platform engineering team.", "tags": ["user:bob"]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert retain.status_code == 200
|
||||||
|
|
||||||
|
response = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories/recall",
|
||||||
|
json={
|
||||||
|
"query": "team membership",
|
||||||
|
"budget": "mid",
|
||||||
|
"tag_groups": [
|
||||||
|
{"tags": ["user:alice"], "match": "any_strict"},
|
||||||
|
{"not": {"tags": ["archived"], "match": "any_strict"}},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
texts = [r["text"] for r in response.json()["results"]]
|
||||||
|
|
||||||
|
assert any("data science" in t for t in texts), "Should find Alice's active memory"
|
||||||
|
assert not any("analytics project" in t for t in texts), "Should NOT find archived memory"
|
||||||
|
assert not any("Bob" in t for t in texts), "Should NOT find Bob (wrong user)"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tag_groups_nested_and_containing_or(api_client):
|
||||||
|
"""
|
||||||
|
Nested: user:alice AND (step:5 OR step:8).
|
||||||
|
|
||||||
|
Retain:
|
||||||
|
- Memory A: tags=[user:alice, step:5] ← should match
|
||||||
|
- Memory B: tags=[user:alice, step:8] ← should match
|
||||||
|
- Memory C: tags=[user:alice, step:9] ← excluded (wrong step)
|
||||||
|
- Memory D: tags=[user:bob, step:5] ← excluded (wrong user)
|
||||||
|
|
||||||
|
tag_groups = [{and: [{tags:[user:alice]}, {or:[{tags:[step:5]},{tags:[step:8]}]}]}]
|
||||||
|
Expected: A and B only.
|
||||||
|
"""
|
||||||
|
bank_id = f"tg_nested_{datetime.now().timestamp()}"
|
||||||
|
|
||||||
|
retain = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories",
|
||||||
|
json={
|
||||||
|
"items": [
|
||||||
|
{"content": "Alice passed the verification at step 5.", "tags": ["user:alice", "step:5"]},
|
||||||
|
{"content": "Alice passed the verification at step 8.", "tags": ["user:alice", "step:8"]},
|
||||||
|
{"content": "Alice passed the verification at step 9.", "tags": ["user:alice", "step:9"]},
|
||||||
|
{"content": "Bob passed the verification at step 5.", "tags": ["user:bob", "step:5"]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert retain.status_code == 200
|
||||||
|
|
||||||
|
response = await api_client.post(
|
||||||
|
f"/v1/default/banks/{bank_id}/memories/recall",
|
||||||
|
json={
|
||||||
|
"query": "verification step completion",
|
||||||
|
"budget": "mid",
|
||||||
|
"tag_groups": [
|
||||||
|
{"and": [
|
||||||
|
{"tags": ["user:alice"], "match": "all_strict"},
|
||||||
|
{"or": [
|
||||||
|
{"tags": ["step:5"], "match": "any_strict"},
|
||||||
|
{"tags": ["step:8"], "match": "any_strict"},
|
||||||
|
]},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
texts = [r["text"] for r in response.json()["results"]]
|
||||||
|
|
||||||
|
assert any("Alice" in t and "step 5" in t for t in texts), "Should find Alice step:5"
|
||||||
|
assert any("Alice" in t and "step 8" in t for t in texts), "Should find Alice step:8"
|
||||||
|
assert not any("step 9" in t for t in texts), "Should NOT find step 9"
|
||||||
|
assert not any("Bob" in t for t in texts), "Should NOT find Bob"
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,7 @@ impl App {
|
||||||
include: None,
|
include: None,
|
||||||
tags: None,
|
tags: None,
|
||||||
tags_match: TagsMatch::Any,
|
tags_match: TagsMatch::Any,
|
||||||
|
tag_groups: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = client.recall(&bank_id, &request, false)
|
let result = client.recall(&bank_id, &request, false)
|
||||||
|
|
@ -361,6 +362,7 @@ impl App {
|
||||||
response_schema: None,
|
response_schema: None,
|
||||||
tags: None,
|
tags: None,
|
||||||
tags_match: TagsMatch::Any,
|
tags_match: TagsMatch::Any,
|
||||||
|
tag_groups: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = client.reflect(&bank_id, &request, false)
|
let result = client.reflect(&bank_id, &request, false)
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ pub fn recall(
|
||||||
include,
|
include,
|
||||||
tags: None,
|
tags: None,
|
||||||
tags_match: TagsMatch::Any,
|
tags_match: TagsMatch::Any,
|
||||||
|
tag_groups: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = client.recall(agent_id, &request, verbose);
|
let response = client.recall(agent_id, &request, verbose);
|
||||||
|
|
@ -340,6 +341,7 @@ pub fn reflect(
|
||||||
response_schema,
|
response_schema,
|
||||||
tags: None,
|
tags: None,
|
||||||
tags_match: TagsMatch::Any,
|
tags_match: TagsMatch::Any,
|
||||||
|
tag_groups: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = client.reflect(agent_id, &request, verbose);
|
let response = client.reflect(agent_id, &request, verbose);
|
||||||
|
|
|
||||||
|
|
@ -4314,6 +4314,11 @@ components:
|
||||||
- all_strict
|
- all_strict
|
||||||
title: Tags Match
|
title: Tags Match
|
||||||
type: string
|
type: string
|
||||||
|
tag_groups:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RecallRequest_tag_groups_inner'
|
||||||
|
nullable: true
|
||||||
|
type: array
|
||||||
required:
|
required:
|
||||||
- query
|
- query
|
||||||
title: RecallRequest
|
title: RecallRequest
|
||||||
|
|
@ -4620,6 +4625,11 @@ components:
|
||||||
- all_strict
|
- all_strict
|
||||||
title: Tags Match
|
title: Tags Match
|
||||||
type: string
|
type: string
|
||||||
|
tag_groups:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RecallRequest_tag_groups_inner'
|
||||||
|
nullable: true
|
||||||
|
type: array
|
||||||
required:
|
required:
|
||||||
- query
|
- query
|
||||||
title: ReflectRequest
|
title: ReflectRequest
|
||||||
|
|
@ -4829,6 +4839,53 @@ components:
|
||||||
title: Max Tokens Per Observation
|
title: Max Tokens Per Observation
|
||||||
type: integer
|
type: integer
|
||||||
title: SourceFactsIncludeOptions
|
title: SourceFactsIncludeOptions
|
||||||
|
TagGroupAnd:
|
||||||
|
description: "Compound AND group: all child filters must match."
|
||||||
|
properties:
|
||||||
|
and:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RecallRequest_tag_groups_inner'
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- and
|
||||||
|
title: TagGroupAnd
|
||||||
|
TagGroupLeaf:
|
||||||
|
description: "A leaf tag filter: matches memories by tag list and match mode."
|
||||||
|
properties:
|
||||||
|
tags:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
match:
|
||||||
|
default: any_strict
|
||||||
|
enum:
|
||||||
|
- any
|
||||||
|
- all
|
||||||
|
- any_strict
|
||||||
|
- all_strict
|
||||||
|
title: Match
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- tags
|
||||||
|
title: TagGroupLeaf
|
||||||
|
TagGroupNot:
|
||||||
|
description: "Compound NOT group: child filter must NOT match."
|
||||||
|
properties:
|
||||||
|
not:
|
||||||
|
$ref: '#/components/schemas/Not'
|
||||||
|
required:
|
||||||
|
- not
|
||||||
|
title: TagGroupNot
|
||||||
|
TagGroupOr:
|
||||||
|
description: "Compound OR group: at least one child filter must match."
|
||||||
|
properties:
|
||||||
|
or:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RecallRequest_tag_groups_inner'
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- or
|
||||||
|
title: TagGroupOr
|
||||||
TagItem:
|
TagItem:
|
||||||
description: Single tag with usage count.
|
description: Single tag with usage count.
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -5324,6 +5381,19 @@ components:
|
||||||
\ which combinations to use."
|
\ which combinations to use."
|
||||||
nullable: true
|
nullable: true
|
||||||
title: ObservationScopes
|
title: ObservationScopes
|
||||||
|
RecallRequest_tag_groups_inner:
|
||||||
|
anyOf:
|
||||||
|
- $ref: '#/components/schemas/TagGroupLeaf'
|
||||||
|
- $ref: '#/components/schemas/TagGroupAnd'
|
||||||
|
- $ref: '#/components/schemas/TagGroupOr'
|
||||||
|
- $ref: '#/components/schemas/TagGroupNot'
|
||||||
|
Not:
|
||||||
|
anyOf:
|
||||||
|
- $ref: '#/components/schemas/TagGroupLeaf'
|
||||||
|
- $ref: '#/components/schemas/TagGroupAnd'
|
||||||
|
- $ref: '#/components/schemas/TagGroupOr'
|
||||||
|
- $ref: '#/components/schemas/TagGroupNot'
|
||||||
|
title: Not
|
||||||
ValidationError_loc_inner:
|
ValidationError_loc_inner:
|
||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
|
|
|
||||||
143
hindsight-clients/go/model_not.go
Normal file
143
hindsight-clients/go/model_not.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
API version: 0.4.17
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package hindsight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Not struct for Not
|
||||||
|
type Not struct {
|
||||||
|
TagGroupAnd *TagGroupAnd
|
||||||
|
TagGroupLeaf *TagGroupLeaf
|
||||||
|
TagGroupNot *TagGroupNot
|
||||||
|
TagGroupOr *TagGroupOr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal JSON data into any of the pointers in the struct
|
||||||
|
func (dst *Not) UnmarshalJSON(data []byte) error {
|
||||||
|
var err error
|
||||||
|
// try to unmarshal JSON data into TagGroupAnd
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupAnd);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupAnd, _ := json.Marshal(dst.TagGroupAnd)
|
||||||
|
if string(jsonTagGroupAnd) == "{}" { // empty struct
|
||||||
|
dst.TagGroupAnd = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupAnd, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupAnd = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to unmarshal JSON data into TagGroupLeaf
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupLeaf);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupLeaf, _ := json.Marshal(dst.TagGroupLeaf)
|
||||||
|
if string(jsonTagGroupLeaf) == "{}" { // empty struct
|
||||||
|
dst.TagGroupLeaf = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupLeaf, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupLeaf = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to unmarshal JSON data into TagGroupNot
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupNot);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupNot, _ := json.Marshal(dst.TagGroupNot)
|
||||||
|
if string(jsonTagGroupNot) == "{}" { // empty struct
|
||||||
|
dst.TagGroupNot = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupNot, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupNot = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to unmarshal JSON data into TagGroupOr
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupOr);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupOr, _ := json.Marshal(dst.TagGroupOr)
|
||||||
|
if string(jsonTagGroupOr) == "{}" { // empty struct
|
||||||
|
dst.TagGroupOr = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupOr, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupOr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("data failed to match schemas in anyOf(Not)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal data from the first non-nil pointers in the struct to JSON
|
||||||
|
func (src *Not) MarshalJSON() ([]byte, error) {
|
||||||
|
if src.TagGroupAnd != nil {
|
||||||
|
return json.Marshal(&src.TagGroupAnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.TagGroupLeaf != nil {
|
||||||
|
return json.Marshal(&src.TagGroupLeaf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.TagGroupNot != nil {
|
||||||
|
return json.Marshal(&src.TagGroupNot)
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.TagGroupOr != nil {
|
||||||
|
return json.Marshal(&src.TagGroupOr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil // no data in anyOf schemas
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type NullableNot struct {
|
||||||
|
value *Not
|
||||||
|
isSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableNot) Get() *Not {
|
||||||
|
return v.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableNot) Set(val *Not) {
|
||||||
|
v.value = val
|
||||||
|
v.isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableNot) IsSet() bool {
|
||||||
|
return v.isSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableNot) Unset() {
|
||||||
|
v.value = nil
|
||||||
|
v.isSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullableNot(val *Not) *NullableNot {
|
||||||
|
return &NullableNot{value: val, isSet: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableNot) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableNot) UnmarshalJSON(src []byte) error {
|
||||||
|
v.isSet = true
|
||||||
|
return json.Unmarshal(src, &v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,6 +32,7 @@ type RecallRequest struct {
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
// How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
// How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
||||||
TagsMatch *string `json:"tags_match,omitempty"`
|
TagsMatch *string `json:"tags_match,omitempty"`
|
||||||
|
TagGroups []RecallRequestTagGroupsInner `json:"tag_groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type _RecallRequest RecallRequest
|
type _RecallRequest RecallRequest
|
||||||
|
|
@ -358,6 +359,39 @@ func (o *RecallRequest) SetTagsMatch(v string) {
|
||||||
o.TagsMatch = &v
|
o.TagsMatch = &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTagGroups returns the TagGroups field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||||
|
func (o *RecallRequest) GetTagGroups() []RecallRequestTagGroupsInner {
|
||||||
|
if o == nil {
|
||||||
|
var ret []RecallRequestTagGroupsInner
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
return o.TagGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagGroupsOk returns a tuple with the TagGroups field value if set, nil otherwise
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||||
|
func (o *RecallRequest) GetTagGroupsOk() ([]RecallRequestTagGroupsInner, bool) {
|
||||||
|
if o == nil || IsNil(o.TagGroups) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return o.TagGroups, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasTagGroups returns a boolean if a field has been set.
|
||||||
|
func (o *RecallRequest) HasTagGroups() bool {
|
||||||
|
if o != nil && !IsNil(o.TagGroups) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTagGroups gets a reference to the given []RecallRequestTagGroupsInner and assigns it to the TagGroups field.
|
||||||
|
func (o *RecallRequest) SetTagGroups(v []RecallRequestTagGroupsInner) {
|
||||||
|
o.TagGroups = v
|
||||||
|
}
|
||||||
|
|
||||||
func (o RecallRequest) MarshalJSON() ([]byte, error) {
|
func (o RecallRequest) MarshalJSON() ([]byte, error) {
|
||||||
toSerialize,err := o.ToMap()
|
toSerialize,err := o.ToMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -393,6 +427,9 @@ func (o RecallRequest) ToMap() (map[string]interface{}, error) {
|
||||||
if !IsNil(o.TagsMatch) {
|
if !IsNil(o.TagsMatch) {
|
||||||
toSerialize["tags_match"] = o.TagsMatch
|
toSerialize["tags_match"] = o.TagsMatch
|
||||||
}
|
}
|
||||||
|
if o.TagGroups != nil {
|
||||||
|
toSerialize["tag_groups"] = o.TagGroups
|
||||||
|
}
|
||||||
return toSerialize, nil
|
return toSerialize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
143
hindsight-clients/go/model_recall_request_tag_groups_inner.go
Normal file
143
hindsight-clients/go/model_recall_request_tag_groups_inner.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
API version: 0.4.17
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package hindsight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// RecallRequestTagGroupsInner struct for RecallRequestTagGroupsInner
|
||||||
|
type RecallRequestTagGroupsInner struct {
|
||||||
|
TagGroupAnd *TagGroupAnd
|
||||||
|
TagGroupLeaf *TagGroupLeaf
|
||||||
|
TagGroupNot *TagGroupNot
|
||||||
|
TagGroupOr *TagGroupOr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal JSON data into any of the pointers in the struct
|
||||||
|
func (dst *RecallRequestTagGroupsInner) UnmarshalJSON(data []byte) error {
|
||||||
|
var err error
|
||||||
|
// try to unmarshal JSON data into TagGroupAnd
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupAnd);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupAnd, _ := json.Marshal(dst.TagGroupAnd)
|
||||||
|
if string(jsonTagGroupAnd) == "{}" { // empty struct
|
||||||
|
dst.TagGroupAnd = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupAnd, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupAnd = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to unmarshal JSON data into TagGroupLeaf
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupLeaf);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupLeaf, _ := json.Marshal(dst.TagGroupLeaf)
|
||||||
|
if string(jsonTagGroupLeaf) == "{}" { // empty struct
|
||||||
|
dst.TagGroupLeaf = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupLeaf, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupLeaf = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to unmarshal JSON data into TagGroupNot
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupNot);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupNot, _ := json.Marshal(dst.TagGroupNot)
|
||||||
|
if string(jsonTagGroupNot) == "{}" { // empty struct
|
||||||
|
dst.TagGroupNot = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupNot, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupNot = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to unmarshal JSON data into TagGroupOr
|
||||||
|
err = json.Unmarshal(data, &dst.TagGroupOr);
|
||||||
|
if err == nil {
|
||||||
|
jsonTagGroupOr, _ := json.Marshal(dst.TagGroupOr)
|
||||||
|
if string(jsonTagGroupOr) == "{}" { // empty struct
|
||||||
|
dst.TagGroupOr = nil
|
||||||
|
} else {
|
||||||
|
return nil // data stored in dst.TagGroupOr, return on the first match
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst.TagGroupOr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("data failed to match schemas in anyOf(RecallRequestTagGroupsInner)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal data from the first non-nil pointers in the struct to JSON
|
||||||
|
func (src *RecallRequestTagGroupsInner) MarshalJSON() ([]byte, error) {
|
||||||
|
if src.TagGroupAnd != nil {
|
||||||
|
return json.Marshal(&src.TagGroupAnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.TagGroupLeaf != nil {
|
||||||
|
return json.Marshal(&src.TagGroupLeaf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.TagGroupNot != nil {
|
||||||
|
return json.Marshal(&src.TagGroupNot)
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.TagGroupOr != nil {
|
||||||
|
return json.Marshal(&src.TagGroupOr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil // no data in anyOf schemas
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type NullableRecallRequestTagGroupsInner struct {
|
||||||
|
value *RecallRequestTagGroupsInner
|
||||||
|
isSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableRecallRequestTagGroupsInner) Get() *RecallRequestTagGroupsInner {
|
||||||
|
return v.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableRecallRequestTagGroupsInner) Set(val *RecallRequestTagGroupsInner) {
|
||||||
|
v.value = val
|
||||||
|
v.isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableRecallRequestTagGroupsInner) IsSet() bool {
|
||||||
|
return v.isSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableRecallRequestTagGroupsInner) Unset() {
|
||||||
|
v.value = nil
|
||||||
|
v.isSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullableRecallRequestTagGroupsInner(val *RecallRequestTagGroupsInner) *NullableRecallRequestTagGroupsInner {
|
||||||
|
return &NullableRecallRequestTagGroupsInner{value: val, isSet: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableRecallRequestTagGroupsInner) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableRecallRequestTagGroupsInner) UnmarshalJSON(src []byte) error {
|
||||||
|
v.isSet = true
|
||||||
|
return json.Unmarshal(src, &v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,6 +32,7 @@ type ReflectRequest struct {
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
// How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
// How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
||||||
TagsMatch *string `json:"tags_match,omitempty"`
|
TagsMatch *string `json:"tags_match,omitempty"`
|
||||||
|
TagGroups []RecallRequestTagGroupsInner `json:"tag_groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type _ReflectRequest ReflectRequest
|
type _ReflectRequest ReflectRequest
|
||||||
|
|
@ -322,6 +323,39 @@ func (o *ReflectRequest) SetTagsMatch(v string) {
|
||||||
o.TagsMatch = &v
|
o.TagsMatch = &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTagGroups returns the TagGroups field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||||
|
func (o *ReflectRequest) GetTagGroups() []RecallRequestTagGroupsInner {
|
||||||
|
if o == nil {
|
||||||
|
var ret []RecallRequestTagGroupsInner
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
return o.TagGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagGroupsOk returns a tuple with the TagGroups field value if set, nil otherwise
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||||
|
func (o *ReflectRequest) GetTagGroupsOk() ([]RecallRequestTagGroupsInner, bool) {
|
||||||
|
if o == nil || IsNil(o.TagGroups) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return o.TagGroups, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasTagGroups returns a boolean if a field has been set.
|
||||||
|
func (o *ReflectRequest) HasTagGroups() bool {
|
||||||
|
if o != nil && !IsNil(o.TagGroups) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTagGroups gets a reference to the given []RecallRequestTagGroupsInner and assigns it to the TagGroups field.
|
||||||
|
func (o *ReflectRequest) SetTagGroups(v []RecallRequestTagGroupsInner) {
|
||||||
|
o.TagGroups = v
|
||||||
|
}
|
||||||
|
|
||||||
func (o ReflectRequest) MarshalJSON() ([]byte, error) {
|
func (o ReflectRequest) MarshalJSON() ([]byte, error) {
|
||||||
toSerialize,err := o.ToMap()
|
toSerialize,err := o.ToMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -354,6 +388,9 @@ func (o ReflectRequest) ToMap() (map[string]interface{}, error) {
|
||||||
if !IsNil(o.TagsMatch) {
|
if !IsNil(o.TagsMatch) {
|
||||||
toSerialize["tags_match"] = o.TagsMatch
|
toSerialize["tags_match"] = o.TagsMatch
|
||||||
}
|
}
|
||||||
|
if o.TagGroups != nil {
|
||||||
|
toSerialize["tag_groups"] = o.TagGroups
|
||||||
|
}
|
||||||
return toSerialize, nil
|
return toSerialize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
158
hindsight-clients/go/model_tag_group_and.go
Normal file
158
hindsight-clients/go/model_tag_group_and.go
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
API version: 0.4.17
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package hindsight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checks if the TagGroupAnd type satisfies the MappedNullable interface at compile time
|
||||||
|
var _ MappedNullable = &TagGroupAnd{}
|
||||||
|
|
||||||
|
// TagGroupAnd Compound AND group: all child filters must match.
|
||||||
|
type TagGroupAnd struct {
|
||||||
|
And []RecallRequestTagGroupsInner `json:"and"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type _TagGroupAnd TagGroupAnd
|
||||||
|
|
||||||
|
// NewTagGroupAnd instantiates a new TagGroupAnd object
|
||||||
|
// This constructor will assign default values to properties that have it defined,
|
||||||
|
// and makes sure properties required by API are set, but the set of arguments
|
||||||
|
// will change when the set of required properties is changed
|
||||||
|
func NewTagGroupAnd(and []RecallRequestTagGroupsInner) *TagGroupAnd {
|
||||||
|
this := TagGroupAnd{}
|
||||||
|
this.And = and
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagGroupAndWithDefaults instantiates a new TagGroupAnd object
|
||||||
|
// This constructor will only assign default values to properties that have it defined,
|
||||||
|
// but it doesn't guarantee that properties required by API are set
|
||||||
|
func NewTagGroupAndWithDefaults() *TagGroupAnd {
|
||||||
|
this := TagGroupAnd{}
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnd returns the And field value
|
||||||
|
func (o *TagGroupAnd) GetAnd() []RecallRequestTagGroupsInner {
|
||||||
|
if o == nil {
|
||||||
|
var ret []RecallRequestTagGroupsInner
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.And
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAndOk returns a tuple with the And field value
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
func (o *TagGroupAnd) GetAndOk() ([]RecallRequestTagGroupsInner, bool) {
|
||||||
|
if o == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return o.And, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAnd sets field value
|
||||||
|
func (o *TagGroupAnd) SetAnd(v []RecallRequestTagGroupsInner) {
|
||||||
|
o.And = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupAnd) MarshalJSON() ([]byte, error) {
|
||||||
|
toSerialize,err := o.ToMap()
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
return json.Marshal(toSerialize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupAnd) ToMap() (map[string]interface{}, error) {
|
||||||
|
toSerialize := map[string]interface{}{}
|
||||||
|
toSerialize["and"] = o.And
|
||||||
|
return toSerialize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TagGroupAnd) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
// This validates that all required properties are included in the JSON object
|
||||||
|
// by unmarshalling the object into a generic map with string keys and checking
|
||||||
|
// that every required field exists as a key in the generic map.
|
||||||
|
requiredProperties := []string{
|
||||||
|
"and",
|
||||||
|
}
|
||||||
|
|
||||||
|
allProperties := make(map[string]interface{})
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &allProperties)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, requiredProperty := range(requiredProperties) {
|
||||||
|
if _, exists := allProperties[requiredProperty]; !exists {
|
||||||
|
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
varTagGroupAnd := _TagGroupAnd{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err = decoder.Decode(&varTagGroupAnd)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*o = TagGroupAnd(varTagGroupAnd)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullableTagGroupAnd struct {
|
||||||
|
value *TagGroupAnd
|
||||||
|
isSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupAnd) Get() *TagGroupAnd {
|
||||||
|
return v.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupAnd) Set(val *TagGroupAnd) {
|
||||||
|
v.value = val
|
||||||
|
v.isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupAnd) IsSet() bool {
|
||||||
|
return v.isSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupAnd) Unset() {
|
||||||
|
v.value = nil
|
||||||
|
v.isSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullableTagGroupAnd(val *TagGroupAnd) *NullableTagGroupAnd {
|
||||||
|
return &NullableTagGroupAnd{value: val, isSet: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupAnd) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupAnd) UnmarshalJSON(src []byte) error {
|
||||||
|
v.isSet = true
|
||||||
|
return json.Unmarshal(src, &v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
198
hindsight-clients/go/model_tag_group_leaf.go
Normal file
198
hindsight-clients/go/model_tag_group_leaf.go
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
API version: 0.4.17
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package hindsight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checks if the TagGroupLeaf type satisfies the MappedNullable interface at compile time
|
||||||
|
var _ MappedNullable = &TagGroupLeaf{}
|
||||||
|
|
||||||
|
// TagGroupLeaf A leaf tag filter: matches memories by tag list and match mode.
|
||||||
|
type TagGroupLeaf struct {
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Match *string `json:"match,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type _TagGroupLeaf TagGroupLeaf
|
||||||
|
|
||||||
|
// NewTagGroupLeaf instantiates a new TagGroupLeaf object
|
||||||
|
// This constructor will assign default values to properties that have it defined,
|
||||||
|
// and makes sure properties required by API are set, but the set of arguments
|
||||||
|
// will change when the set of required properties is changed
|
||||||
|
func NewTagGroupLeaf(tags []string) *TagGroupLeaf {
|
||||||
|
this := TagGroupLeaf{}
|
||||||
|
this.Tags = tags
|
||||||
|
var match string = "any_strict"
|
||||||
|
this.Match = &match
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagGroupLeafWithDefaults instantiates a new TagGroupLeaf object
|
||||||
|
// This constructor will only assign default values to properties that have it defined,
|
||||||
|
// but it doesn't guarantee that properties required by API are set
|
||||||
|
func NewTagGroupLeafWithDefaults() *TagGroupLeaf {
|
||||||
|
this := TagGroupLeaf{}
|
||||||
|
var match string = "any_strict"
|
||||||
|
this.Match = &match
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTags returns the Tags field value
|
||||||
|
func (o *TagGroupLeaf) GetTags() []string {
|
||||||
|
if o == nil {
|
||||||
|
var ret []string
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagsOk returns a tuple with the Tags field value
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
func (o *TagGroupLeaf) GetTagsOk() ([]string, bool) {
|
||||||
|
if o == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return o.Tags, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTags sets field value
|
||||||
|
func (o *TagGroupLeaf) SetTags(v []string) {
|
||||||
|
o.Tags = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMatch returns the Match field value if set, zero value otherwise.
|
||||||
|
func (o *TagGroupLeaf) GetMatch() string {
|
||||||
|
if o == nil || IsNil(o.Match) {
|
||||||
|
var ret string
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
return *o.Match
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMatchOk returns a tuple with the Match field value if set, nil otherwise
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
func (o *TagGroupLeaf) GetMatchOk() (*string, bool) {
|
||||||
|
if o == nil || IsNil(o.Match) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return o.Match, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMatch returns a boolean if a field has been set.
|
||||||
|
func (o *TagGroupLeaf) HasMatch() bool {
|
||||||
|
if o != nil && !IsNil(o.Match) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMatch gets a reference to the given string and assigns it to the Match field.
|
||||||
|
func (o *TagGroupLeaf) SetMatch(v string) {
|
||||||
|
o.Match = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupLeaf) MarshalJSON() ([]byte, error) {
|
||||||
|
toSerialize,err := o.ToMap()
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
return json.Marshal(toSerialize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupLeaf) ToMap() (map[string]interface{}, error) {
|
||||||
|
toSerialize := map[string]interface{}{}
|
||||||
|
toSerialize["tags"] = o.Tags
|
||||||
|
if !IsNil(o.Match) {
|
||||||
|
toSerialize["match"] = o.Match
|
||||||
|
}
|
||||||
|
return toSerialize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TagGroupLeaf) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
// This validates that all required properties are included in the JSON object
|
||||||
|
// by unmarshalling the object into a generic map with string keys and checking
|
||||||
|
// that every required field exists as a key in the generic map.
|
||||||
|
requiredProperties := []string{
|
||||||
|
"tags",
|
||||||
|
}
|
||||||
|
|
||||||
|
allProperties := make(map[string]interface{})
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &allProperties)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, requiredProperty := range(requiredProperties) {
|
||||||
|
if _, exists := allProperties[requiredProperty]; !exists {
|
||||||
|
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
varTagGroupLeaf := _TagGroupLeaf{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err = decoder.Decode(&varTagGroupLeaf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*o = TagGroupLeaf(varTagGroupLeaf)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullableTagGroupLeaf struct {
|
||||||
|
value *TagGroupLeaf
|
||||||
|
isSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupLeaf) Get() *TagGroupLeaf {
|
||||||
|
return v.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupLeaf) Set(val *TagGroupLeaf) {
|
||||||
|
v.value = val
|
||||||
|
v.isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupLeaf) IsSet() bool {
|
||||||
|
return v.isSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupLeaf) Unset() {
|
||||||
|
v.value = nil
|
||||||
|
v.isSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullableTagGroupLeaf(val *TagGroupLeaf) *NullableTagGroupLeaf {
|
||||||
|
return &NullableTagGroupLeaf{value: val, isSet: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupLeaf) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupLeaf) UnmarshalJSON(src []byte) error {
|
||||||
|
v.isSet = true
|
||||||
|
return json.Unmarshal(src, &v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
158
hindsight-clients/go/model_tag_group_not.go
Normal file
158
hindsight-clients/go/model_tag_group_not.go
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
API version: 0.4.17
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package hindsight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checks if the TagGroupNot type satisfies the MappedNullable interface at compile time
|
||||||
|
var _ MappedNullable = &TagGroupNot{}
|
||||||
|
|
||||||
|
// TagGroupNot Compound NOT group: child filter must NOT match.
|
||||||
|
type TagGroupNot struct {
|
||||||
|
Not Not `json:"not"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type _TagGroupNot TagGroupNot
|
||||||
|
|
||||||
|
// NewTagGroupNot instantiates a new TagGroupNot object
|
||||||
|
// This constructor will assign default values to properties that have it defined,
|
||||||
|
// and makes sure properties required by API are set, but the set of arguments
|
||||||
|
// will change when the set of required properties is changed
|
||||||
|
func NewTagGroupNot(not Not) *TagGroupNot {
|
||||||
|
this := TagGroupNot{}
|
||||||
|
this.Not = not
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagGroupNotWithDefaults instantiates a new TagGroupNot object
|
||||||
|
// This constructor will only assign default values to properties that have it defined,
|
||||||
|
// but it doesn't guarantee that properties required by API are set
|
||||||
|
func NewTagGroupNotWithDefaults() *TagGroupNot {
|
||||||
|
this := TagGroupNot{}
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNot returns the Not field value
|
||||||
|
func (o *TagGroupNot) GetNot() Not {
|
||||||
|
if o == nil {
|
||||||
|
var ret Not
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.Not
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotOk returns a tuple with the Not field value
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
func (o *TagGroupNot) GetNotOk() (*Not, bool) {
|
||||||
|
if o == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return &o.Not, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNot sets field value
|
||||||
|
func (o *TagGroupNot) SetNot(v Not) {
|
||||||
|
o.Not = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupNot) MarshalJSON() ([]byte, error) {
|
||||||
|
toSerialize,err := o.ToMap()
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
return json.Marshal(toSerialize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupNot) ToMap() (map[string]interface{}, error) {
|
||||||
|
toSerialize := map[string]interface{}{}
|
||||||
|
toSerialize["not"] = o.Not
|
||||||
|
return toSerialize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TagGroupNot) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
// This validates that all required properties are included in the JSON object
|
||||||
|
// by unmarshalling the object into a generic map with string keys and checking
|
||||||
|
// that every required field exists as a key in the generic map.
|
||||||
|
requiredProperties := []string{
|
||||||
|
"not",
|
||||||
|
}
|
||||||
|
|
||||||
|
allProperties := make(map[string]interface{})
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &allProperties)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, requiredProperty := range(requiredProperties) {
|
||||||
|
if _, exists := allProperties[requiredProperty]; !exists {
|
||||||
|
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
varTagGroupNot := _TagGroupNot{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err = decoder.Decode(&varTagGroupNot)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*o = TagGroupNot(varTagGroupNot)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullableTagGroupNot struct {
|
||||||
|
value *TagGroupNot
|
||||||
|
isSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupNot) Get() *TagGroupNot {
|
||||||
|
return v.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupNot) Set(val *TagGroupNot) {
|
||||||
|
v.value = val
|
||||||
|
v.isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupNot) IsSet() bool {
|
||||||
|
return v.isSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupNot) Unset() {
|
||||||
|
v.value = nil
|
||||||
|
v.isSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullableTagGroupNot(val *TagGroupNot) *NullableTagGroupNot {
|
||||||
|
return &NullableTagGroupNot{value: val, isSet: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupNot) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupNot) UnmarshalJSON(src []byte) error {
|
||||||
|
v.isSet = true
|
||||||
|
return json.Unmarshal(src, &v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
158
hindsight-clients/go/model_tag_group_or.go
Normal file
158
hindsight-clients/go/model_tag_group_or.go
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
API version: 0.4.17
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||||
|
|
||||||
|
package hindsight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checks if the TagGroupOr type satisfies the MappedNullable interface at compile time
|
||||||
|
var _ MappedNullable = &TagGroupOr{}
|
||||||
|
|
||||||
|
// TagGroupOr Compound OR group: at least one child filter must match.
|
||||||
|
type TagGroupOr struct {
|
||||||
|
Or []RecallRequestTagGroupsInner `json:"or"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type _TagGroupOr TagGroupOr
|
||||||
|
|
||||||
|
// NewTagGroupOr instantiates a new TagGroupOr object
|
||||||
|
// This constructor will assign default values to properties that have it defined,
|
||||||
|
// and makes sure properties required by API are set, but the set of arguments
|
||||||
|
// will change when the set of required properties is changed
|
||||||
|
func NewTagGroupOr(or []RecallRequestTagGroupsInner) *TagGroupOr {
|
||||||
|
this := TagGroupOr{}
|
||||||
|
this.Or = or
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagGroupOrWithDefaults instantiates a new TagGroupOr object
|
||||||
|
// This constructor will only assign default values to properties that have it defined,
|
||||||
|
// but it doesn't guarantee that properties required by API are set
|
||||||
|
func NewTagGroupOrWithDefaults() *TagGroupOr {
|
||||||
|
this := TagGroupOr{}
|
||||||
|
return &this
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOr returns the Or field value
|
||||||
|
func (o *TagGroupOr) GetOr() []RecallRequestTagGroupsInner {
|
||||||
|
if o == nil {
|
||||||
|
var ret []RecallRequestTagGroupsInner
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.Or
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrOk returns a tuple with the Or field value
|
||||||
|
// and a boolean to check if the value has been set.
|
||||||
|
func (o *TagGroupOr) GetOrOk() ([]RecallRequestTagGroupsInner, bool) {
|
||||||
|
if o == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return o.Or, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOr sets field value
|
||||||
|
func (o *TagGroupOr) SetOr(v []RecallRequestTagGroupsInner) {
|
||||||
|
o.Or = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupOr) MarshalJSON() ([]byte, error) {
|
||||||
|
toSerialize,err := o.ToMap()
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
return json.Marshal(toSerialize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TagGroupOr) ToMap() (map[string]interface{}, error) {
|
||||||
|
toSerialize := map[string]interface{}{}
|
||||||
|
toSerialize["or"] = o.Or
|
||||||
|
return toSerialize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TagGroupOr) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
// This validates that all required properties are included in the JSON object
|
||||||
|
// by unmarshalling the object into a generic map with string keys and checking
|
||||||
|
// that every required field exists as a key in the generic map.
|
||||||
|
requiredProperties := []string{
|
||||||
|
"or",
|
||||||
|
}
|
||||||
|
|
||||||
|
allProperties := make(map[string]interface{})
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &allProperties)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, requiredProperty := range(requiredProperties) {
|
||||||
|
if _, exists := allProperties[requiredProperty]; !exists {
|
||||||
|
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
varTagGroupOr := _TagGroupOr{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err = decoder.Decode(&varTagGroupOr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*o = TagGroupOr(varTagGroupOr)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullableTagGroupOr struct {
|
||||||
|
value *TagGroupOr
|
||||||
|
isSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupOr) Get() *TagGroupOr {
|
||||||
|
return v.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupOr) Set(val *TagGroupOr) {
|
||||||
|
v.value = val
|
||||||
|
v.isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupOr) IsSet() bool {
|
||||||
|
return v.isSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupOr) Unset() {
|
||||||
|
v.value = nil
|
||||||
|
v.isSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullableTagGroupOr(val *TagGroupOr) *NullableTagGroupOr {
|
||||||
|
return &NullableTagGroupOr{value: val, isSet: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NullableTagGroupOr) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NullableTagGroupOr) UnmarshalJSON(src []byte) error {
|
||||||
|
v.isSet = true
|
||||||
|
return json.Unmarshal(src, &v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -62,11 +62,13 @@ hindsight_client_api/models/memory_item.py
|
||||||
hindsight_client_api/models/mental_model_list_response.py
|
hindsight_client_api/models/mental_model_list_response.py
|
||||||
hindsight_client_api/models/mental_model_response.py
|
hindsight_client_api/models/mental_model_response.py
|
||||||
hindsight_client_api/models/mental_model_trigger.py
|
hindsight_client_api/models/mental_model_trigger.py
|
||||||
|
hindsight_client_api/models/model_not.py
|
||||||
hindsight_client_api/models/observation_scopes.py
|
hindsight_client_api/models/observation_scopes.py
|
||||||
hindsight_client_api/models/operation_response.py
|
hindsight_client_api/models/operation_response.py
|
||||||
hindsight_client_api/models/operation_status_response.py
|
hindsight_client_api/models/operation_status_response.py
|
||||||
hindsight_client_api/models/operations_list_response.py
|
hindsight_client_api/models/operations_list_response.py
|
||||||
hindsight_client_api/models/recall_request.py
|
hindsight_client_api/models/recall_request.py
|
||||||
|
hindsight_client_api/models/recall_request_tag_groups_inner.py
|
||||||
hindsight_client_api/models/recall_response.py
|
hindsight_client_api/models/recall_response.py
|
||||||
hindsight_client_api/models/recall_result.py
|
hindsight_client_api/models/recall_result.py
|
||||||
hindsight_client_api/models/reflect_based_on.py
|
hindsight_client_api/models/reflect_based_on.py
|
||||||
|
|
@ -83,6 +85,10 @@ hindsight_client_api/models/retain_request.py
|
||||||
hindsight_client_api/models/retain_response.py
|
hindsight_client_api/models/retain_response.py
|
||||||
hindsight_client_api/models/retry_operation_response.py
|
hindsight_client_api/models/retry_operation_response.py
|
||||||
hindsight_client_api/models/source_facts_include_options.py
|
hindsight_client_api/models/source_facts_include_options.py
|
||||||
|
hindsight_client_api/models/tag_group_and.py
|
||||||
|
hindsight_client_api/models/tag_group_leaf.py
|
||||||
|
hindsight_client_api/models/tag_group_not.py
|
||||||
|
hindsight_client_api/models/tag_group_or.py
|
||||||
hindsight_client_api/models/tag_item.py
|
hindsight_client_api/models/tag_item.py
|
||||||
hindsight_client_api/models/timestamp.py
|
hindsight_client_api/models/timestamp.py
|
||||||
hindsight_client_api/models/token_usage.py
|
hindsight_client_api/models/token_usage.py
|
||||||
|
|
|
||||||
|
|
@ -87,11 +87,13 @@ from hindsight_client_api.models.memory_item import MemoryItem
|
||||||
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
|
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
|
||||||
from hindsight_client_api.models.mental_model_response import MentalModelResponse
|
from hindsight_client_api.models.mental_model_response import MentalModelResponse
|
||||||
from hindsight_client_api.models.mental_model_trigger import MentalModelTrigger
|
from hindsight_client_api.models.mental_model_trigger import MentalModelTrigger
|
||||||
|
from hindsight_client_api.models.model_not import ModelNot
|
||||||
from hindsight_client_api.models.observation_scopes import ObservationScopes
|
from hindsight_client_api.models.observation_scopes import ObservationScopes
|
||||||
from hindsight_client_api.models.operation_response import OperationResponse
|
from hindsight_client_api.models.operation_response import OperationResponse
|
||||||
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
|
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
|
||||||
from hindsight_client_api.models.operations_list_response import OperationsListResponse
|
from hindsight_client_api.models.operations_list_response import OperationsListResponse
|
||||||
from hindsight_client_api.models.recall_request import RecallRequest
|
from hindsight_client_api.models.recall_request import RecallRequest
|
||||||
|
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
|
||||||
from hindsight_client_api.models.recall_response import RecallResponse
|
from hindsight_client_api.models.recall_response import RecallResponse
|
||||||
from hindsight_client_api.models.recall_result import RecallResult
|
from hindsight_client_api.models.recall_result import RecallResult
|
||||||
from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
|
from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
|
||||||
|
|
@ -108,6 +110,10 @@ from hindsight_client_api.models.retain_request import RetainRequest
|
||||||
from hindsight_client_api.models.retain_response import RetainResponse
|
from hindsight_client_api.models.retain_response import RetainResponse
|
||||||
from hindsight_client_api.models.retry_operation_response import RetryOperationResponse
|
from hindsight_client_api.models.retry_operation_response import RetryOperationResponse
|
||||||
from hindsight_client_api.models.source_facts_include_options import SourceFactsIncludeOptions
|
from hindsight_client_api.models.source_facts_include_options import SourceFactsIncludeOptions
|
||||||
|
from hindsight_client_api.models.tag_group_and import TagGroupAnd
|
||||||
|
from hindsight_client_api.models.tag_group_leaf import TagGroupLeaf
|
||||||
|
from hindsight_client_api.models.tag_group_not import TagGroupNot
|
||||||
|
from hindsight_client_api.models.tag_group_or import TagGroupOr
|
||||||
from hindsight_client_api.models.tag_item import TagItem
|
from hindsight_client_api.models.tag_item import TagItem
|
||||||
from hindsight_client_api.models.timestamp import Timestamp
|
from hindsight_client_api.models.timestamp import Timestamp
|
||||||
from hindsight_client_api.models.token_usage import TokenUsage
|
from hindsight_client_api.models.token_usage import TokenUsage
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,13 @@ from hindsight_client_api.models.memory_item import MemoryItem
|
||||||
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
|
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
|
||||||
from hindsight_client_api.models.mental_model_response import MentalModelResponse
|
from hindsight_client_api.models.mental_model_response import MentalModelResponse
|
||||||
from hindsight_client_api.models.mental_model_trigger import MentalModelTrigger
|
from hindsight_client_api.models.mental_model_trigger import MentalModelTrigger
|
||||||
|
from hindsight_client_api.models.model_not import ModelNot
|
||||||
from hindsight_client_api.models.observation_scopes import ObservationScopes
|
from hindsight_client_api.models.observation_scopes import ObservationScopes
|
||||||
from hindsight_client_api.models.operation_response import OperationResponse
|
from hindsight_client_api.models.operation_response import OperationResponse
|
||||||
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
|
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
|
||||||
from hindsight_client_api.models.operations_list_response import OperationsListResponse
|
from hindsight_client_api.models.operations_list_response import OperationsListResponse
|
||||||
from hindsight_client_api.models.recall_request import RecallRequest
|
from hindsight_client_api.models.recall_request import RecallRequest
|
||||||
|
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
|
||||||
from hindsight_client_api.models.recall_response import RecallResponse
|
from hindsight_client_api.models.recall_response import RecallResponse
|
||||||
from hindsight_client_api.models.recall_result import RecallResult
|
from hindsight_client_api.models.recall_result import RecallResult
|
||||||
from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
|
from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
|
||||||
|
|
@ -82,6 +84,10 @@ from hindsight_client_api.models.retain_request import RetainRequest
|
||||||
from hindsight_client_api.models.retain_response import RetainResponse
|
from hindsight_client_api.models.retain_response import RetainResponse
|
||||||
from hindsight_client_api.models.retry_operation_response import RetryOperationResponse
|
from hindsight_client_api.models.retry_operation_response import RetryOperationResponse
|
||||||
from hindsight_client_api.models.source_facts_include_options import SourceFactsIncludeOptions
|
from hindsight_client_api.models.source_facts_include_options import SourceFactsIncludeOptions
|
||||||
|
from hindsight_client_api.models.tag_group_and import TagGroupAnd
|
||||||
|
from hindsight_client_api.models.tag_group_leaf import TagGroupLeaf
|
||||||
|
from hindsight_client_api.models.tag_group_not import TagGroupNot
|
||||||
|
from hindsight_client_api.models.tag_group_or import TagGroupOr
|
||||||
from hindsight_client_api.models.tag_item import TagItem
|
from hindsight_client_api.models.tag_item import TagItem
|
||||||
from hindsight_client_api.models.timestamp import Timestamp
|
from hindsight_client_api.models.timestamp import Timestamp
|
||||||
from hindsight_client_api.models.token_usage import TokenUsage
|
from hindsight_client_api.models.token_usage import TokenUsage
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 0.4.17
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from inspect import getfullargspec
|
||||||
|
import json
|
||||||
|
import pprint
|
||||||
|
import re # noqa: F401
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator
|
||||||
|
from typing import Optional
|
||||||
|
from hindsight_client_api.models.tag_group_leaf import TagGroupLeaf
|
||||||
|
from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict
|
||||||
|
from typing_extensions import Literal, Self
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
MODELNOT_ANY_OF_SCHEMAS = ["TagGroupAnd", "TagGroupLeaf", "TagGroupNot", "TagGroupOr"]
|
||||||
|
|
||||||
|
class ModelNot(BaseModel):
|
||||||
|
"""
|
||||||
|
ModelNot
|
||||||
|
"""
|
||||||
|
|
||||||
|
# data type: TagGroupLeaf
|
||||||
|
anyof_schema_1_validator: Optional[TagGroupLeaf] = None
|
||||||
|
# data type: TagGroupAnd
|
||||||
|
anyof_schema_2_validator: Optional[TagGroupAnd] = None
|
||||||
|
# data type: TagGroupOr
|
||||||
|
anyof_schema_3_validator: Optional[TagGroupOr] = None
|
||||||
|
# data type: TagGroupNot
|
||||||
|
anyof_schema_4_validator: Optional[TagGroupNot] = None
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
actual_instance: Optional[Union[TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr]] = None
|
||||||
|
else:
|
||||||
|
actual_instance: Any = None
|
||||||
|
any_of_schemas: Set[str] = { "TagGroupAnd", "TagGroupLeaf", "TagGroupNot", "TagGroupOr" }
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"validate_assignment": True,
|
||||||
|
"protected_namespaces": (),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
if args:
|
||||||
|
if len(args) > 1:
|
||||||
|
raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`")
|
||||||
|
if kwargs:
|
||||||
|
raise ValueError("If a position argument is used, keyword arguments cannot be used.")
|
||||||
|
super().__init__(actual_instance=args[0])
|
||||||
|
else:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@field_validator('actual_instance')
|
||||||
|
def actual_instance_must_validate_anyof(cls, v):
|
||||||
|
instance = ModelNot.model_construct()
|
||||||
|
error_messages = []
|
||||||
|
# validate data type: TagGroupLeaf
|
||||||
|
if not isinstance(v, TagGroupLeaf):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupLeaf`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
# validate data type: TagGroupAnd
|
||||||
|
if not isinstance(v, TagGroupAnd):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupAnd`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
# validate data type: TagGroupOr
|
||||||
|
if not isinstance(v, TagGroupOr):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupOr`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
# validate data type: TagGroupNot
|
||||||
|
if not isinstance(v, TagGroupNot):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupNot`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
if error_messages:
|
||||||
|
# no match
|
||||||
|
raise ValueError("No match found when setting the actual_instance in ModelNot with anyOf schemas: TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr. Details: " + ", ".join(error_messages))
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, obj: Dict[str, Any]) -> Self:
|
||||||
|
return cls.from_json(json.dumps(obj))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str: str) -> Self:
|
||||||
|
"""Returns the object represented by the json string"""
|
||||||
|
instance = cls.model_construct()
|
||||||
|
error_messages = []
|
||||||
|
# anyof_schema_1_validator: Optional[TagGroupLeaf] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupLeaf.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
# anyof_schema_2_validator: Optional[TagGroupAnd] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupAnd.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
# anyof_schema_3_validator: Optional[TagGroupOr] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupOr.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
# anyof_schema_4_validator: Optional[TagGroupNot] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupNot.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
|
||||||
|
if error_messages:
|
||||||
|
# no match
|
||||||
|
raise ValueError("No match found when deserializing the JSON string into ModelNot with anyOf schemas: TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr. Details: " + ", ".join(error_messages))
|
||||||
|
else:
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Returns the JSON representation of the actual instance"""
|
||||||
|
if self.actual_instance is None:
|
||||||
|
return "null"
|
||||||
|
|
||||||
|
if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json):
|
||||||
|
return self.actual_instance.to_json()
|
||||||
|
else:
|
||||||
|
return json.dumps(self.actual_instance)
|
||||||
|
|
||||||
|
def to_dict(self) -> Optional[Union[Dict[str, Any], TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr]]:
|
||||||
|
"""Returns the dict representation of the actual instance"""
|
||||||
|
if self.actual_instance is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict):
|
||||||
|
return self.actual_instance.to_dict()
|
||||||
|
else:
|
||||||
|
return self.actual_instance
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
"""Returns the string representation of the actual instance"""
|
||||||
|
return pprint.pformat(self.model_dump())
|
||||||
|
|
||||||
|
from hindsight_client_api.models.tag_group_and import TagGroupAnd
|
||||||
|
from hindsight_client_api.models.tag_group_not import TagGroupNot
|
||||||
|
from hindsight_client_api.models.tag_group_or import TagGroupOr
|
||||||
|
# TODO: Rewrite to not use raise_errors
|
||||||
|
ModelNot.model_rebuild(raise_errors=False)
|
||||||
|
|
||||||
|
|
@ -21,6 +21,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, Strict
|
||||||
from typing import Any, ClassVar, Dict, List, Optional
|
from typing import Any, ClassVar, Dict, List, Optional
|
||||||
from hindsight_client_api.models.budget import Budget
|
from hindsight_client_api.models.budget import Budget
|
||||||
from hindsight_client_api.models.include_options import IncludeOptions
|
from hindsight_client_api.models.include_options import IncludeOptions
|
||||||
|
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
@ -37,7 +38,8 @@ class RecallRequest(BaseModel):
|
||||||
include: Optional[IncludeOptions] = Field(default=None, description="Options for including additional data (entities are included by default)")
|
include: Optional[IncludeOptions] = Field(default=None, description="Options for including additional data (entities are included by default)")
|
||||||
tags: Optional[List[StrictStr]] = None
|
tags: Optional[List[StrictStr]] = None
|
||||||
tags_match: Optional[StrictStr] = Field(default='any', description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).")
|
tags_match: Optional[StrictStr] = Field(default='any', description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).")
|
||||||
__properties: ClassVar[List[str]] = ["query", "types", "budget", "max_tokens", "trace", "query_timestamp", "include", "tags", "tags_match"]
|
tag_groups: Optional[List[RecallRequestTagGroupsInner]] = None
|
||||||
|
__properties: ClassVar[List[str]] = ["query", "types", "budget", "max_tokens", "trace", "query_timestamp", "include", "tags", "tags_match", "tag_groups"]
|
||||||
|
|
||||||
@field_validator('tags_match')
|
@field_validator('tags_match')
|
||||||
def tags_match_validate_enum(cls, value):
|
def tags_match_validate_enum(cls, value):
|
||||||
|
|
@ -91,6 +93,13 @@ class RecallRequest(BaseModel):
|
||||||
# override the default output from pydantic by calling `to_dict()` of include
|
# override the default output from pydantic by calling `to_dict()` of include
|
||||||
if self.include:
|
if self.include:
|
||||||
_dict['include'] = self.include.to_dict()
|
_dict['include'] = self.include.to_dict()
|
||||||
|
# override the default output from pydantic by calling `to_dict()` of each item in tag_groups (list)
|
||||||
|
_items = []
|
||||||
|
if self.tag_groups:
|
||||||
|
for _item_tag_groups in self.tag_groups:
|
||||||
|
if _item_tag_groups:
|
||||||
|
_items.append(_item_tag_groups.to_dict())
|
||||||
|
_dict['tag_groups'] = _items
|
||||||
# set to None if types (nullable) is None
|
# set to None if types (nullable) is None
|
||||||
# and model_fields_set contains the field
|
# and model_fields_set contains the field
|
||||||
if self.types is None and "types" in self.model_fields_set:
|
if self.types is None and "types" in self.model_fields_set:
|
||||||
|
|
@ -106,6 +115,11 @@ class RecallRequest(BaseModel):
|
||||||
if self.tags is None and "tags" in self.model_fields_set:
|
if self.tags is None and "tags" in self.model_fields_set:
|
||||||
_dict['tags'] = None
|
_dict['tags'] = None
|
||||||
|
|
||||||
|
# set to None if tag_groups (nullable) is None
|
||||||
|
# and model_fields_set contains the field
|
||||||
|
if self.tag_groups is None and "tag_groups" in self.model_fields_set:
|
||||||
|
_dict['tag_groups'] = None
|
||||||
|
|
||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -126,7 +140,8 @@ class RecallRequest(BaseModel):
|
||||||
"query_timestamp": obj.get("query_timestamp"),
|
"query_timestamp": obj.get("query_timestamp"),
|
||||||
"include": IncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None,
|
"include": IncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None,
|
||||||
"tags": obj.get("tags"),
|
"tags": obj.get("tags"),
|
||||||
"tags_match": obj.get("tags_match") if obj.get("tags_match") is not None else 'any'
|
"tags_match": obj.get("tags_match") if obj.get("tags_match") is not None else 'any',
|
||||||
|
"tag_groups": [RecallRequestTagGroupsInner.from_dict(_item) for _item in obj["tag_groups"]] if obj.get("tag_groups") is not None else None
|
||||||
})
|
})
|
||||||
return _obj
|
return _obj
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 0.4.17
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from inspect import getfullargspec
|
||||||
|
import json
|
||||||
|
import pprint
|
||||||
|
import re # noqa: F401
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator
|
||||||
|
from typing import Optional
|
||||||
|
from hindsight_client_api.models.tag_group_leaf import TagGroupLeaf
|
||||||
|
from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict
|
||||||
|
from typing_extensions import Literal, Self
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
RECALLREQUESTTAGGROUPSINNER_ANY_OF_SCHEMAS = ["TagGroupAnd", "TagGroupLeaf", "TagGroupNot", "TagGroupOr"]
|
||||||
|
|
||||||
|
class RecallRequestTagGroupsInner(BaseModel):
|
||||||
|
"""
|
||||||
|
RecallRequestTagGroupsInner
|
||||||
|
"""
|
||||||
|
|
||||||
|
# data type: TagGroupLeaf
|
||||||
|
anyof_schema_1_validator: Optional[TagGroupLeaf] = None
|
||||||
|
# data type: TagGroupAnd
|
||||||
|
anyof_schema_2_validator: Optional[TagGroupAnd] = None
|
||||||
|
# data type: TagGroupOr
|
||||||
|
anyof_schema_3_validator: Optional[TagGroupOr] = None
|
||||||
|
# data type: TagGroupNot
|
||||||
|
anyof_schema_4_validator: Optional[TagGroupNot] = None
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
actual_instance: Optional[Union[TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr]] = None
|
||||||
|
else:
|
||||||
|
actual_instance: Any = None
|
||||||
|
any_of_schemas: Set[str] = { "TagGroupAnd", "TagGroupLeaf", "TagGroupNot", "TagGroupOr" }
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"validate_assignment": True,
|
||||||
|
"protected_namespaces": (),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
if args:
|
||||||
|
if len(args) > 1:
|
||||||
|
raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`")
|
||||||
|
if kwargs:
|
||||||
|
raise ValueError("If a position argument is used, keyword arguments cannot be used.")
|
||||||
|
super().__init__(actual_instance=args[0])
|
||||||
|
else:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@field_validator('actual_instance')
|
||||||
|
def actual_instance_must_validate_anyof(cls, v):
|
||||||
|
instance = RecallRequestTagGroupsInner.model_construct()
|
||||||
|
error_messages = []
|
||||||
|
# validate data type: TagGroupLeaf
|
||||||
|
if not isinstance(v, TagGroupLeaf):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupLeaf`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
# validate data type: TagGroupAnd
|
||||||
|
if not isinstance(v, TagGroupAnd):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupAnd`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
# validate data type: TagGroupOr
|
||||||
|
if not isinstance(v, TagGroupOr):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupOr`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
# validate data type: TagGroupNot
|
||||||
|
if not isinstance(v, TagGroupNot):
|
||||||
|
error_messages.append(f"Error! Input type `{type(v)}` is not `TagGroupNot`")
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
if error_messages:
|
||||||
|
# no match
|
||||||
|
raise ValueError("No match found when setting the actual_instance in RecallRequestTagGroupsInner with anyOf schemas: TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr. Details: " + ", ".join(error_messages))
|
||||||
|
else:
|
||||||
|
return v
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, obj: Dict[str, Any]) -> Self:
|
||||||
|
return cls.from_json(json.dumps(obj))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str: str) -> Self:
|
||||||
|
"""Returns the object represented by the json string"""
|
||||||
|
instance = cls.model_construct()
|
||||||
|
error_messages = []
|
||||||
|
# anyof_schema_1_validator: Optional[TagGroupLeaf] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupLeaf.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
# anyof_schema_2_validator: Optional[TagGroupAnd] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupAnd.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
# anyof_schema_3_validator: Optional[TagGroupOr] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupOr.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
# anyof_schema_4_validator: Optional[TagGroupNot] = None
|
||||||
|
try:
|
||||||
|
instance.actual_instance = TagGroupNot.from_json(json_str)
|
||||||
|
return instance
|
||||||
|
except (ValidationError, ValueError) as e:
|
||||||
|
error_messages.append(str(e))
|
||||||
|
|
||||||
|
if error_messages:
|
||||||
|
# no match
|
||||||
|
raise ValueError("No match found when deserializing the JSON string into RecallRequestTagGroupsInner with anyOf schemas: TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr. Details: " + ", ".join(error_messages))
|
||||||
|
else:
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Returns the JSON representation of the actual instance"""
|
||||||
|
if self.actual_instance is None:
|
||||||
|
return "null"
|
||||||
|
|
||||||
|
if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json):
|
||||||
|
return self.actual_instance.to_json()
|
||||||
|
else:
|
||||||
|
return json.dumps(self.actual_instance)
|
||||||
|
|
||||||
|
def to_dict(self) -> Optional[Union[Dict[str, Any], TagGroupAnd, TagGroupLeaf, TagGroupNot, TagGroupOr]]:
|
||||||
|
"""Returns the dict representation of the actual instance"""
|
||||||
|
if self.actual_instance is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict):
|
||||||
|
return self.actual_instance.to_dict()
|
||||||
|
else:
|
||||||
|
return self.actual_instance
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
"""Returns the string representation of the actual instance"""
|
||||||
|
return pprint.pformat(self.model_dump())
|
||||||
|
|
||||||
|
from hindsight_client_api.models.tag_group_and import TagGroupAnd
|
||||||
|
from hindsight_client_api.models.tag_group_not import TagGroupNot
|
||||||
|
from hindsight_client_api.models.tag_group_or import TagGroupOr
|
||||||
|
# TODO: Rewrite to not use raise_errors
|
||||||
|
RecallRequestTagGroupsInner.model_rebuild(raise_errors=False)
|
||||||
|
|
||||||
|
|
@ -20,6 +20,7 @@ import json
|
||||||
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator
|
||||||
from typing import Any, ClassVar, Dict, List, Optional
|
from typing import Any, ClassVar, Dict, List, Optional
|
||||||
from hindsight_client_api.models.budget import Budget
|
from hindsight_client_api.models.budget import Budget
|
||||||
|
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
|
||||||
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions
|
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
@ -36,7 +37,8 @@ class ReflectRequest(BaseModel):
|
||||||
response_schema: Optional[Dict[str, Any]] = None
|
response_schema: Optional[Dict[str, Any]] = None
|
||||||
tags: Optional[List[StrictStr]] = None
|
tags: Optional[List[StrictStr]] = None
|
||||||
tags_match: Optional[StrictStr] = Field(default='any', description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).")
|
tags_match: Optional[StrictStr] = Field(default='any', description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).")
|
||||||
__properties: ClassVar[List[str]] = ["query", "budget", "context", "max_tokens", "include", "response_schema", "tags", "tags_match"]
|
tag_groups: Optional[List[RecallRequestTagGroupsInner]] = None
|
||||||
|
__properties: ClassVar[List[str]] = ["query", "budget", "context", "max_tokens", "include", "response_schema", "tags", "tags_match", "tag_groups"]
|
||||||
|
|
||||||
@field_validator('tags_match')
|
@field_validator('tags_match')
|
||||||
def tags_match_validate_enum(cls, value):
|
def tags_match_validate_enum(cls, value):
|
||||||
|
|
@ -90,6 +92,13 @@ class ReflectRequest(BaseModel):
|
||||||
# override the default output from pydantic by calling `to_dict()` of include
|
# override the default output from pydantic by calling `to_dict()` of include
|
||||||
if self.include:
|
if self.include:
|
||||||
_dict['include'] = self.include.to_dict()
|
_dict['include'] = self.include.to_dict()
|
||||||
|
# override the default output from pydantic by calling `to_dict()` of each item in tag_groups (list)
|
||||||
|
_items = []
|
||||||
|
if self.tag_groups:
|
||||||
|
for _item_tag_groups in self.tag_groups:
|
||||||
|
if _item_tag_groups:
|
||||||
|
_items.append(_item_tag_groups.to_dict())
|
||||||
|
_dict['tag_groups'] = _items
|
||||||
# set to None if context (nullable) is None
|
# set to None if context (nullable) is None
|
||||||
# and model_fields_set contains the field
|
# and model_fields_set contains the field
|
||||||
if self.context is None and "context" in self.model_fields_set:
|
if self.context is None and "context" in self.model_fields_set:
|
||||||
|
|
@ -105,6 +114,11 @@ class ReflectRequest(BaseModel):
|
||||||
if self.tags is None and "tags" in self.model_fields_set:
|
if self.tags is None and "tags" in self.model_fields_set:
|
||||||
_dict['tags'] = None
|
_dict['tags'] = None
|
||||||
|
|
||||||
|
# set to None if tag_groups (nullable) is None
|
||||||
|
# and model_fields_set contains the field
|
||||||
|
if self.tag_groups is None and "tag_groups" in self.model_fields_set:
|
||||||
|
_dict['tag_groups'] = None
|
||||||
|
|
||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -124,7 +138,8 @@ class ReflectRequest(BaseModel):
|
||||||
"include": ReflectIncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None,
|
"include": ReflectIncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None,
|
||||||
"response_schema": obj.get("response_schema"),
|
"response_schema": obj.get("response_schema"),
|
||||||
"tags": obj.get("tags"),
|
"tags": obj.get("tags"),
|
||||||
"tags_match": obj.get("tags_match") if obj.get("tags_match") is not None else 'any'
|
"tags_match": obj.get("tags_match") if obj.get("tags_match") is not None else 'any',
|
||||||
|
"tag_groups": [RecallRequestTagGroupsInner.from_dict(_item) for _item in obj["tag_groups"]] if obj.get("tag_groups") is not None else None
|
||||||
})
|
})
|
||||||
return _obj
|
return _obj
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 0.4.17
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import pprint
|
||||||
|
import re # noqa: F401
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from typing import Any, ClassVar, Dict, List
|
||||||
|
from typing import Optional, Set
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
class TagGroupAnd(BaseModel):
|
||||||
|
"""
|
||||||
|
Compound AND group: all child filters must match.
|
||||||
|
""" # noqa: E501
|
||||||
|
var_and: List[RecallRequestTagGroupsInner] = Field(alias="and")
|
||||||
|
__properties: ClassVar[List[str]] = ["and"]
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
protected_namespaces=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
"""Returns the string representation of the model using alias"""
|
||||||
|
return pprint.pformat(self.model_dump(by_alias=True))
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Returns the JSON representation of the model using alias"""
|
||||||
|
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupAnd from a JSON string"""
|
||||||
|
return cls.from_dict(json.loads(json_str))
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Return the dictionary representation of the model using alias.
|
||||||
|
|
||||||
|
This has the following differences from calling pydantic's
|
||||||
|
`self.model_dump(by_alias=True)`:
|
||||||
|
|
||||||
|
* `None` is only added to the output dict for nullable fields that
|
||||||
|
were set at model initialization. Other fields with value `None`
|
||||||
|
are ignored.
|
||||||
|
"""
|
||||||
|
excluded_fields: Set[str] = set([
|
||||||
|
])
|
||||||
|
|
||||||
|
_dict = self.model_dump(
|
||||||
|
by_alias=True,
|
||||||
|
exclude=excluded_fields,
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
# override the default output from pydantic by calling `to_dict()` of each item in var_and (list)
|
||||||
|
_items = []
|
||||||
|
if self.var_and:
|
||||||
|
for _item_var_and in self.var_and:
|
||||||
|
if _item_var_and:
|
||||||
|
_items.append(_item_var_and.to_dict())
|
||||||
|
_dict['and'] = _items
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupAnd from a dict"""
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return cls.model_validate(obj)
|
||||||
|
|
||||||
|
_obj = cls.model_validate({
|
||||||
|
"and": [RecallRequestTagGroupsInner.from_dict(_item) for _item in obj["and"]] if obj.get("and") is not None else None
|
||||||
|
})
|
||||||
|
return _obj
|
||||||
|
|
||||||
|
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
|
||||||
|
# TODO: Rewrite to not use raise_errors
|
||||||
|
TagGroupAnd.model_rebuild(raise_errors=False)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 0.4.17
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import pprint
|
||||||
|
import re # noqa: F401
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, StrictStr, field_validator
|
||||||
|
from typing import Any, ClassVar, Dict, List, Optional
|
||||||
|
from typing import Optional, Set
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
class TagGroupLeaf(BaseModel):
|
||||||
|
"""
|
||||||
|
A leaf tag filter: matches memories by tag list and match mode.
|
||||||
|
""" # noqa: E501
|
||||||
|
tags: List[StrictStr]
|
||||||
|
match: Optional[StrictStr] = 'any_strict'
|
||||||
|
__properties: ClassVar[List[str]] = ["tags", "match"]
|
||||||
|
|
||||||
|
@field_validator('match')
|
||||||
|
def match_validate_enum(cls, value):
|
||||||
|
"""Validates the enum"""
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if value not in set(['any', 'all', 'any_strict', 'all_strict']):
|
||||||
|
raise ValueError("must be one of enum values ('any', 'all', 'any_strict', 'all_strict')")
|
||||||
|
return value
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
protected_namespaces=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
"""Returns the string representation of the model using alias"""
|
||||||
|
return pprint.pformat(self.model_dump(by_alias=True))
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Returns the JSON representation of the model using alias"""
|
||||||
|
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupLeaf from a JSON string"""
|
||||||
|
return cls.from_dict(json.loads(json_str))
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Return the dictionary representation of the model using alias.
|
||||||
|
|
||||||
|
This has the following differences from calling pydantic's
|
||||||
|
`self.model_dump(by_alias=True)`:
|
||||||
|
|
||||||
|
* `None` is only added to the output dict for nullable fields that
|
||||||
|
were set at model initialization. Other fields with value `None`
|
||||||
|
are ignored.
|
||||||
|
"""
|
||||||
|
excluded_fields: Set[str] = set([
|
||||||
|
])
|
||||||
|
|
||||||
|
_dict = self.model_dump(
|
||||||
|
by_alias=True,
|
||||||
|
exclude=excluded_fields,
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupLeaf from a dict"""
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return cls.model_validate(obj)
|
||||||
|
|
||||||
|
_obj = cls.model_validate({
|
||||||
|
"tags": obj.get("tags"),
|
||||||
|
"match": obj.get("match") if obj.get("match") is not None else 'any_strict'
|
||||||
|
})
|
||||||
|
return _obj
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 0.4.17
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import pprint
|
||||||
|
import re # noqa: F401
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from typing import Any, ClassVar, Dict, List
|
||||||
|
from typing import Optional, Set
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
class TagGroupNot(BaseModel):
|
||||||
|
"""
|
||||||
|
Compound NOT group: child filter must NOT match.
|
||||||
|
""" # noqa: E501
|
||||||
|
var_not: ModelNot = Field(alias="not")
|
||||||
|
__properties: ClassVar[List[str]] = ["not"]
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
protected_namespaces=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
"""Returns the string representation of the model using alias"""
|
||||||
|
return pprint.pformat(self.model_dump(by_alias=True))
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Returns the JSON representation of the model using alias"""
|
||||||
|
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupNot from a JSON string"""
|
||||||
|
return cls.from_dict(json.loads(json_str))
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Return the dictionary representation of the model using alias.
|
||||||
|
|
||||||
|
This has the following differences from calling pydantic's
|
||||||
|
`self.model_dump(by_alias=True)`:
|
||||||
|
|
||||||
|
* `None` is only added to the output dict for nullable fields that
|
||||||
|
were set at model initialization. Other fields with value `None`
|
||||||
|
are ignored.
|
||||||
|
"""
|
||||||
|
excluded_fields: Set[str] = set([
|
||||||
|
])
|
||||||
|
|
||||||
|
_dict = self.model_dump(
|
||||||
|
by_alias=True,
|
||||||
|
exclude=excluded_fields,
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
# override the default output from pydantic by calling `to_dict()` of var_not
|
||||||
|
if self.var_not:
|
||||||
|
_dict['not'] = self.var_not.to_dict()
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupNot from a dict"""
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return cls.model_validate(obj)
|
||||||
|
|
||||||
|
_obj = cls.model_validate({
|
||||||
|
"not": ModelNot.from_dict(obj["not"]) if obj.get("not") is not None else None
|
||||||
|
})
|
||||||
|
return _obj
|
||||||
|
|
||||||
|
from hindsight_client_api.models.model_not import ModelNot
|
||||||
|
# TODO: Rewrite to not use raise_errors
|
||||||
|
TagGroupNot.model_rebuild(raise_errors=False)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 0.4.17
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import pprint
|
||||||
|
import re # noqa: F401
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from typing import Any, ClassVar, Dict, List
|
||||||
|
from typing import Optional, Set
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
class TagGroupOr(BaseModel):
|
||||||
|
"""
|
||||||
|
Compound OR group: at least one child filter must match.
|
||||||
|
""" # noqa: E501
|
||||||
|
var_or: List[RecallRequestTagGroupsInner] = Field(alias="or")
|
||||||
|
__properties: ClassVar[List[str]] = ["or"]
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
protected_namespaces=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
"""Returns the string representation of the model using alias"""
|
||||||
|
return pprint.pformat(self.model_dump(by_alias=True))
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Returns the JSON representation of the model using alias"""
|
||||||
|
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupOr from a JSON string"""
|
||||||
|
return cls.from_dict(json.loads(json_str))
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Return the dictionary representation of the model using alias.
|
||||||
|
|
||||||
|
This has the following differences from calling pydantic's
|
||||||
|
`self.model_dump(by_alias=True)`:
|
||||||
|
|
||||||
|
* `None` is only added to the output dict for nullable fields that
|
||||||
|
were set at model initialization. Other fields with value `None`
|
||||||
|
are ignored.
|
||||||
|
"""
|
||||||
|
excluded_fields: Set[str] = set([
|
||||||
|
])
|
||||||
|
|
||||||
|
_dict = self.model_dump(
|
||||||
|
by_alias=True,
|
||||||
|
exclude=excluded_fields,
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
# override the default output from pydantic by calling `to_dict()` of each item in var_or (list)
|
||||||
|
_items = []
|
||||||
|
if self.var_or:
|
||||||
|
for _item_var_or in self.var_or:
|
||||||
|
if _item_var_or:
|
||||||
|
_items.append(_item_var_or.to_dict())
|
||||||
|
_dict['or'] = _items
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||||
|
"""Create an instance of TagGroupOr from a dict"""
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return cls.model_validate(obj)
|
||||||
|
|
||||||
|
_obj = cls.model_validate({
|
||||||
|
"or": [RecallRequestTagGroupsInner.from_dict(_item) for _item in obj["or"]] if obj.get("or") is not None else None
|
||||||
|
})
|
||||||
|
return _obj
|
||||||
|
|
||||||
|
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
|
||||||
|
# TODO: Rewrite to not use raise_errors
|
||||||
|
TagGroupOr.model_rebuild(raise_errors=False)
|
||||||
|
|
||||||
|
|
@ -103,6 +103,7 @@ mod tests {
|
||||||
types: None,
|
types: None,
|
||||||
tags: None,
|
tags: None,
|
||||||
tags_match: types::TagsMatch::Any,
|
tags_match: types::TagsMatch::Any,
|
||||||
|
tag_groups: None,
|
||||||
};
|
};
|
||||||
let recall_response = client
|
let recall_response = client
|
||||||
.recall_memories(&bank_id, None, &recall_request)
|
.recall_memories(&bank_id, None, &recall_request)
|
||||||
|
|
|
||||||
|
|
@ -1488,6 +1488,14 @@ export type RecallRequest = {
|
||||||
* How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
* How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
||||||
*/
|
*/
|
||||||
tags_match?: "any" | "all" | "any_strict" | "all_strict";
|
tags_match?: "any" | "all" | "any_strict" | "all_strict";
|
||||||
|
/**
|
||||||
|
* Tag Groups
|
||||||
|
*
|
||||||
|
* Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.
|
||||||
|
*/
|
||||||
|
tag_groups?: Array<
|
||||||
|
TagGroupLeaf | TagGroupAnd | TagGroupOr | TagGroupNot
|
||||||
|
> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1791,6 +1799,14 @@ export type ReflectRequest = {
|
||||||
* How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
* How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
||||||
*/
|
*/
|
||||||
tags_match?: "any" | "all" | "any_strict" | "all_strict";
|
tags_match?: "any" | "all" | "any_strict" | "all_strict";
|
||||||
|
/**
|
||||||
|
* Tag Groups
|
||||||
|
*
|
||||||
|
* Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.
|
||||||
|
*/
|
||||||
|
tag_groups?: Array<
|
||||||
|
TagGroupLeaf | TagGroupAnd | TagGroupOr | TagGroupNot
|
||||||
|
> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1991,6 +2007,58 @@ export type SourceFactsIncludeOptions = {
|
||||||
max_tokens_per_observation?: number;
|
max_tokens_per_observation?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagGroupAnd
|
||||||
|
*
|
||||||
|
* Compound AND group: all child filters must match.
|
||||||
|
*/
|
||||||
|
export type TagGroupAnd = {
|
||||||
|
/**
|
||||||
|
* And
|
||||||
|
*/
|
||||||
|
and: Array<TagGroupLeaf | TagGroupAnd | TagGroupOr | TagGroupNot>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagGroupLeaf
|
||||||
|
*
|
||||||
|
* A leaf tag filter: matches memories by tag list and match mode.
|
||||||
|
*/
|
||||||
|
export type TagGroupLeaf = {
|
||||||
|
/**
|
||||||
|
* Tags
|
||||||
|
*/
|
||||||
|
tags: Array<string>;
|
||||||
|
/**
|
||||||
|
* Match
|
||||||
|
*/
|
||||||
|
match?: "any" | "all" | "any_strict" | "all_strict";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagGroupNot
|
||||||
|
*
|
||||||
|
* Compound NOT group: child filter must NOT match.
|
||||||
|
*/
|
||||||
|
export type TagGroupNot = {
|
||||||
|
/**
|
||||||
|
* Not
|
||||||
|
*/
|
||||||
|
not: TagGroupLeaf | TagGroupAnd | TagGroupOr | TagGroupNot;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagGroupOr
|
||||||
|
*
|
||||||
|
* Compound OR group: at least one child filter must match.
|
||||||
|
*/
|
||||||
|
export type TagGroupOr = {
|
||||||
|
/**
|
||||||
|
* Or
|
||||||
|
*/
|
||||||
|
or: Array<TagGroupLeaf | TagGroupAnd | TagGroupOr | TagGroupNot>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TagItem
|
* TagItem
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,66 @@ Use this for strict scope enforcement where a memory must explicitly belong to *
|
||||||
A memory with tags `["user:alice", "team", "project:x"]` will still match a filter of `["user:alice", "team"]` under `all_strict` — extra tags on the memory are not a problem. The filter only requires the memory to contain **at least** the specified tags.
|
A memory with tags `["user:alice", "team", "project:x"]` will still match a filter of `["user:alice", "team"]` under `all_strict` — extra tags on the memory are not a problem. The filter only requires the memory to contain **at least** the specified tags.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
### tag_groups
|
||||||
|
|
||||||
|
`tag_groups` is a list of compound boolean tag filters. The groups in the list are AND-ed together at the top level. Each group is a recursive boolean expression: a **leaf** node `{tags, match}`, or a **compound** node `{and: [...]}`, `{or: [...]}`, or `{not: ...}`.
|
||||||
|
|
||||||
|
`tag_groups` and `tags` / `tags_match` can be used simultaneously — they are AND-ed together.
|
||||||
|
|
||||||
|
#### Leaf node
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "tags": ["step:5", "step:8"], "match": "any_strict" }
|
||||||
|
```
|
||||||
|
|
||||||
|
`match` accepts the same values as `tags_match`: `any`, `all`, `any_strict`, `all_strict`. Defaults to `any_strict`.
|
||||||
|
|
||||||
|
#### Compound nodes
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "and": [ <TagGroup>, <TagGroup>, ... ] }
|
||||||
|
{ "or": [ <TagGroup>, <TagGroup>, ... ] }
|
||||||
|
{ "not": <TagGroup> }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
**Step filter AND user scope** — two top-level groups AND-ed:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tag_groups": [
|
||||||
|
{ "tags": ["step:5", "step:8", "step:12"], "match": "any_strict" },
|
||||||
|
{ "tags": ["user:ep_42"], "match": "all_strict" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nested OR inside AND** — user must match, plus either step OR priority:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tag_groups": [
|
||||||
|
{ "tags": ["user:alice"], "match": "all_strict" },
|
||||||
|
{ "or": [
|
||||||
|
{ "tags": ["step:5"], "match": "any_strict" },
|
||||||
|
{ "tags": ["priority:high"], "match": "all_strict" }
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exclusion** — user must match, but archived memories are excluded:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tag_groups": [
|
||||||
|
{ "tags": ["user:alice"], "match": "all_strict" },
|
||||||
|
{ "not": { "tags": ["archived"], "match": "any_strict" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### trace
|
### trace
|
||||||
|
|
||||||
When set to `true`, the response includes a detailed debug trace covering the query embedding, entry points, per-strategy retrieval results, RRF fusion candidates, reranked results, temporal constraints detected, and per-phase timings. Has no effect on the retrieval logic itself. Useful for understanding why specific memories were or were not returned.
|
When set to `true`, the response includes a detailed debug trace covering the query embedding, entry points, per-strategy retrieval results, RRF fusion candidates, reranked results, temporal constraints detected, and per-phase timings. Has no effect on the retrieval logic itself. Useful for understanding why specific memories were or were not returned.
|
||||||
|
|
|
||||||
|
|
@ -6543,6 +6543,34 @@
|
||||||
"title": "Tags Match",
|
"title": "Tags Match",
|
||||||
"description": "How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
"description": "How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
||||||
"default": "any"
|
"default": "any"
|
||||||
|
},
|
||||||
|
"tag_groups": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupLeaf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupAnd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupOr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupNot"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Tag Groups",
|
||||||
|
"description": "Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -7155,6 +7183,34 @@
|
||||||
"title": "Tags Match",
|
"title": "Tags Match",
|
||||||
"description": "How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
"description": "How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
|
||||||
"default": "any"
|
"default": "any"
|
||||||
|
},
|
||||||
|
"tag_groups": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupLeaf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupAnd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupOr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupNot"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Tag Groups",
|
||||||
|
"description": "Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -7547,6 +7603,121 @@
|
||||||
"title": "SourceFactsIncludeOptions",
|
"title": "SourceFactsIncludeOptions",
|
||||||
"description": "Options for including source facts for observation-type results."
|
"description": "Options for including source facts for observation-type results."
|
||||||
},
|
},
|
||||||
|
"TagGroupAnd": {
|
||||||
|
"properties": {
|
||||||
|
"and": {
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupLeaf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupAnd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupOr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupNot"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "And"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"and"
|
||||||
|
],
|
||||||
|
"title": "TagGroupAnd",
|
||||||
|
"description": "Compound AND group: all child filters must match."
|
||||||
|
},
|
||||||
|
"TagGroupLeaf": {
|
||||||
|
"properties": {
|
||||||
|
"tags": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Tags"
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"any",
|
||||||
|
"all",
|
||||||
|
"any_strict",
|
||||||
|
"all_strict"
|
||||||
|
],
|
||||||
|
"title": "Match",
|
||||||
|
"default": "any_strict"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"tags"
|
||||||
|
],
|
||||||
|
"title": "TagGroupLeaf",
|
||||||
|
"description": "A leaf tag filter: matches memories by tag list and match mode."
|
||||||
|
},
|
||||||
|
"TagGroupNot": {
|
||||||
|
"properties": {
|
||||||
|
"not": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupLeaf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupAnd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupOr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupNot"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Not"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"not"
|
||||||
|
],
|
||||||
|
"title": "TagGroupNot",
|
||||||
|
"description": "Compound NOT group: child filter must NOT match."
|
||||||
|
},
|
||||||
|
"TagGroupOr": {
|
||||||
|
"properties": {
|
||||||
|
"or": {
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupLeaf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupAnd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupOr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TagGroupNot"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Or"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"or"
|
||||||
|
],
|
||||||
|
"title": "TagGroupOr",
|
||||||
|
"description": "Compound OR group: at least one child filter must match."
|
||||||
|
},
|
||||||
"TagItem": {
|
"TagItem": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"tag": {
|
"tag": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue