Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afde5d905a | ||
|
|
8af978a397 | ||
|
|
75356d1fed | ||
|
|
f94d3c7a9e | ||
|
|
3ce4ad2835 |
@@ -34,7 +34,7 @@ def _parse_metadata(metadata: Any) -> dict[str, Any]:
|
||||
|
||||
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
|
||||
|
||||
@@ -73,7 +73,7 @@ def FieldWithDefault(default_factory: Callable, **kwargs) -> Any:
|
||||
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.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.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
|
||||
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), "
|
||||
"'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):
|
||||
@@ -639,6 +650,17 @@ class ReflectRequest(BaseModel):
|
||||
description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes 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):
|
||||
@@ -2324,6 +2346,7 @@ def _register_routes(app: FastAPI):
|
||||
request_context=request_context,
|
||||
tags=request.tags,
|
||||
tags_match=request.tags_match,
|
||||
tag_groups=request.tag_groups,
|
||||
)
|
||||
|
||||
# Convert core MemoryFact objects to API RecallResult objects (excluding internal metrics)
|
||||
@@ -2459,6 +2482,7 @@ def _register_routes(app: FastAPI):
|
||||
request_context=request_context,
|
||||
tags=request.tags,
|
||||
tags_match=request.tags_match,
|
||||
tag_groups=request.tag_groups,
|
||||
)
|
||||
|
||||
# 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 .search import think_utils
|
||||
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
|
||||
|
||||
|
||||
@@ -2300,6 +2300,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
request_context: "RequestContext",
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
_connection_budget: int | None = None,
|
||||
_quiet: bool = False,
|
||||
) -> RecallResultModel:
|
||||
@@ -2434,6 +2435,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
semaphore_wait=semaphore_wait,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
connection_budget=_connection_budget,
|
||||
quiet=_quiet,
|
||||
include_source_facts=include_source_facts,
|
||||
@@ -2561,6 +2563,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
semaphore_wait: float = 0.0,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
connection_budget: int | None = None,
|
||||
quiet: bool = False,
|
||||
include_source_facts: bool = False,
|
||||
@@ -2680,6 +2683,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
self.query_analyzer,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
)
|
||||
parallel_duration = time.time() - parallel_start
|
||||
finally:
|
||||
@@ -5040,6 +5044,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
request_context: "RequestContext",
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
exclude_mental_model_ids: list[str] | None = None,
|
||||
_skip_span: bool = False,
|
||||
) -> ReflectResult:
|
||||
@@ -5142,6 +5147,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
max_results=max_results,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
exclude_ids=exclude_mental_model_ids,
|
||||
pending_consolidation=pending_consolidation,
|
||||
)
|
||||
@@ -5155,6 +5161,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
max_tokens=max_tokens,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
last_consolidated_at=last_consolidated_at,
|
||||
pending_consolidation=pending_consolidation,
|
||||
)
|
||||
@@ -5168,6 +5175,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
max_tokens=max_tokens,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
max_chunk_tokens=max_chunk_tokens,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ async def tool_search_mental_models(
|
||||
max_results: int = 5,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: str = "any",
|
||||
tag_groups: "list | None" = None,
|
||||
exclude_ids: list[str] | None = None,
|
||||
pending_consolidation: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
@@ -52,7 +53,7 @@ async def tool_search_mental_models(
|
||||
Dict with matching mental models including content and freshness info
|
||||
"""
|
||||
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
|
||||
filters = ""
|
||||
@@ -65,6 +66,11 @@ async def tool_search_mental_models(
|
||||
filters += f" {tag_clause}"
|
||||
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:
|
||||
filters += f" AND id != ALL(${next_param}::text[])"
|
||||
params.append(exclude_ids)
|
||||
@@ -125,6 +131,7 @@ async def tool_search_observations(
|
||||
max_tokens: int = 5000,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: str = "any",
|
||||
tag_groups: "list | None" = None,
|
||||
last_consolidated_at: datetime | None = None,
|
||||
pending_consolidation: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
@@ -157,6 +164,7 @@ async def tool_search_observations(
|
||||
request_context=request_context,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
include_source_facts=True,
|
||||
max_source_facts_tokens=-1, # No token limit — include all source facts
|
||||
_connection_budget=1,
|
||||
@@ -189,6 +197,7 @@ async def tool_recall(
|
||||
max_tokens: int = 2048,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: str = "any",
|
||||
tag_groups: "list | None" = None,
|
||||
connection_budget: int = 1,
|
||||
max_chunk_tokens: int = 1000,
|
||||
) -> dict[str, Any]:
|
||||
@@ -222,6 +231,7 @@ async def tool_recall(
|
||||
request_context=request_context,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
_connection_budget=connection_budget,
|
||||
_quiet=True, # Suppress logging for internal operations
|
||||
include_chunks=include_chunks,
|
||||
|
||||
@@ -11,7 +11,7 @@ from abc import ABC, abstractmethod
|
||||
|
||||
from ..db_utils import acquire_with_retry
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -46,6 +46,7 @@ class GraphRetriever(ABC):
|
||||
adjacency=None, # TypedAdjacency, optional pre-loaded graph
|
||||
tags: list[str] | None = None, # Visibility scope tags for filtering
|
||||
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]:
|
||||
"""
|
||||
Retrieve relevant facts via graph traversal.
|
||||
@@ -120,6 +121,7 @@ class BFSGraphRetriever(GraphRetriever):
|
||||
adjacency=None, # Not used by BFS
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
||||
"""
|
||||
Retrieve facts using BFS spreading activation.
|
||||
@@ -136,7 +138,14 @@ class BFSGraphRetriever(GraphRetriever):
|
||||
"""
|
||||
async with acquire_with_retry(pool) as 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
|
||||
|
||||
@@ -149,14 +158,18 @@ class BFSGraphRetriever(GraphRetriever):
|
||||
budget: int,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> list[RetrievalResult]:
|
||||
"""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)
|
||||
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]
|
||||
if tags:
|
||||
params.append(tags)
|
||||
params.extend(groups_params)
|
||||
|
||||
# Step 1: Find entry points
|
||||
entry_points = await conn.fetch(
|
||||
@@ -170,6 +183,7 @@ class BFSGraphRetriever(GraphRetriever):
|
||||
AND fact_type = $3
|
||||
AND (1 - (embedding <=> $1::vector)) >= $4
|
||||
{tags_clause}
|
||||
{groups_clause}
|
||||
ORDER BY embedding <=> $1::vector
|
||||
LIMIT $5
|
||||
""",
|
||||
@@ -261,4 +275,8 @@ class BFSGraphRetriever(GraphRetriever):
|
||||
if tags:
|
||||
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
|
||||
|
||||
@@ -28,7 +28,7 @@ import time
|
||||
from ..db_utils import acquire_with_retry
|
||||
from ..memory_engine import fq_table
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -43,14 +43,18 @@ async def _find_semantic_seeds(
|
||||
threshold: float = 0.3,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> list[RetrievalResult]:
|
||||
"""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)
|
||||
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]
|
||||
if tags:
|
||||
params.append(tags)
|
||||
params.extend(groups_params)
|
||||
|
||||
rows = await conn.fetch(
|
||||
f"""
|
||||
@@ -63,6 +67,7 @@ async def _find_semantic_seeds(
|
||||
AND fact_type = $3
|
||||
AND (1 - (embedding <=> $1::vector)) >= $4
|
||||
{tags_clause}
|
||||
{groups_clause}
|
||||
ORDER BY embedding <=> $1::vector
|
||||
LIMIT $5
|
||||
""",
|
||||
@@ -110,6 +115,7 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
adjacency=None,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
||||
"""
|
||||
Retrieve facts by expanding links from seeds.
|
||||
@@ -147,6 +153,7 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
threshold=0.3,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
)
|
||||
timings.seeds_time = time.time() - seeds_start
|
||||
logger.debug(
|
||||
@@ -221,6 +228,9 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
if tags:
|
||||
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.traverse = time.time() - start_time
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from dataclasses import dataclass, field
|
||||
from ..db_utils import acquire_with_retry
|
||||
from ..memory_engine import fq_table
|
||||
from .graph_retrieval import GraphRetriever
|
||||
from .tags import TagsMatch
|
||||
from .tags import TagGroup, TagsMatch
|
||||
from .types import MPFPTimings, RetrievalResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -506,6 +506,7 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||
adjacency=None, # Ignored - kept for interface compatibility
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> tuple[list[RetrievalResult], MPFPTimings | None]:
|
||||
"""
|
||||
Retrieve facts using MPFP algorithm with lazy edge loading.
|
||||
@@ -537,7 +538,13 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||
if not semantic_seed_nodes:
|
||||
seeds_start = time.time()
|
||||
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
|
||||
logger.debug(
|
||||
@@ -616,6 +623,12 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||
|
||||
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)
|
||||
|
||||
# Add activation scores from fusion
|
||||
@@ -656,14 +669,18 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||
threshold: float = 0.3,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> list[SeedNode]:
|
||||
"""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)
|
||||
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]
|
||||
if tags:
|
||||
params.append(tags)
|
||||
params.extend(groups_params)
|
||||
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
rows = await conn.fetch(
|
||||
@@ -675,6 +692,7 @@ class MPFPGraphRetriever(GraphRetriever):
|
||||
AND fact_type = $3
|
||||
AND (1 - (embedding <=> $1::vector)) >= $4
|
||||
{tags_clause}
|
||||
{groups_clause}
|
||||
ORDER BY embedding <=> $1::vector
|
||||
LIMIT $5
|
||||
""",
|
||||
|
||||
@@ -20,7 +20,7 @@ from ..memory_engine import fq_table
|
||||
from .graph_retrieval import BFSGraphRetriever, GraphRetriever
|
||||
from .link_expansion_retrieval import LinkExpansionRetriever
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -94,6 +94,7 @@ async def retrieve_semantic_bm25_combined(
|
||||
limit: int,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> dict[str, tuple[list[RetrievalResult], list[RetrievalResult]]]:
|
||||
"""
|
||||
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)
|
||||
# $4 = bm25_text (only 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_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) ---
|
||||
# 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.
|
||||
@@ -169,6 +175,7 @@ async def retrieve_semantic_bm25_combined(
|
||||
f" AND embedding IS NOT NULL"
|
||||
f" AND (1 - (embedding <=> $1::vector)) >= 0.3"
|
||||
f" {tags_clause}"
|
||||
f" {groups_clause}"
|
||||
f" ORDER BY embedding <=> $1::vector"
|
||||
f" LIMIT {hnsw_fetch})"
|
||||
)
|
||||
@@ -208,6 +215,7 @@ async def retrieve_semantic_bm25_combined(
|
||||
f" AND fact_type = '{ft}'"
|
||||
f" {bm25_where_filter}"
|
||||
f" {tags_clause}"
|
||||
f" {groups_clause}"
|
||||
f" ORDER BY {bm25_order_by}"
|
||||
f" LIMIT $3)"
|
||||
)
|
||||
@@ -219,6 +227,7 @@ async def retrieve_semantic_bm25_combined(
|
||||
params.append(bm25_text_param)
|
||||
if tags:
|
||||
params.append(tags)
|
||||
params.extend(groups_params)
|
||||
|
||||
rows = await conn.fetch(query, *params)
|
||||
|
||||
@@ -251,6 +260,7 @@ async def retrieve_temporal_combined(
|
||||
semantic_threshold: float = 0.1,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> dict[str, list[RetrievalResult]]:
|
||||
"""
|
||||
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)
|
||||
|
||||
# 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)
|
||||
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:
|
||||
params.append(tags)
|
||||
params.extend(groups_params)
|
||||
|
||||
# Two-phase entry point query:
|
||||
# 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)
|
||||
)
|
||||
{tags_clause}
|
||||
{groups_clause}
|
||||
),
|
||||
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,
|
||||
@@ -400,16 +415,21 @@ async def retrieve_temporal_combined(
|
||||
|
||||
# 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_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:
|
||||
iteration += 1
|
||||
batch_ids = 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]
|
||||
if tags:
|
||||
spreading_params.append(tags)
|
||||
spreading_params.extend(spreading_groups_params)
|
||||
|
||||
# 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.
|
||||
@@ -436,6 +456,7 @@ async def retrieve_temporal_combined(
|
||||
AND mu.embedding IS NOT NULL
|
||||
AND (1 - (mu.embedding <=> $1::vector)) >= $4
|
||||
{spreading_tags_clause}
|
||||
{spreading_groups_clause}
|
||||
""",
|
||||
*spreading_params,
|
||||
)
|
||||
@@ -509,6 +530,7 @@ async def retrieve_all_fact_types_parallel(
|
||||
graph_retriever: GraphRetriever | None = None,
|
||||
tags: list[str] | None = None,
|
||||
tags_match: TagsMatch = "any",
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
) -> MultiFactTypeRetrievalResult:
|
||||
"""
|
||||
Optimized retrieval for multiple fact types using batched queries.
|
||||
@@ -566,6 +588,7 @@ async def retrieve_all_fact_types_parallel(
|
||||
thinking_budget,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
)
|
||||
semantic_bm25_time = time.time() - semantic_bm25_start
|
||||
|
||||
@@ -584,6 +607,7 @@ async def retrieve_all_fact_types_parallel(
|
||||
semantic_threshold=0.1,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
)
|
||||
temporal_time = time.time() - temporal_start
|
||||
|
||||
@@ -604,6 +628,7 @@ async def retrieve_all_fact_types_parallel(
|
||||
temporal_seeds=None,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
tag_groups=tag_groups,
|
||||
)
|
||||
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
|
||||
"""
|
||||
|
||||
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"]
|
||||
|
||||
@@ -170,3 +174,217 @@ def filter_results_by_tags(
|
||||
filtered.append(result)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -263,6 +272,327 @@ class TestFilterResultsByTags:
|
||||
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
|
||||
# ============================================================================
|
||||
@@ -956,3 +1286,218 @@ async def test_list_memories_includes_tags(api_client, test_bank_id):
|
||||
assert set(memory_item["tags"]) == set(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,
|
||||
tags: None,
|
||||
tags_match: TagsMatch::Any,
|
||||
tag_groups: None,
|
||||
};
|
||||
|
||||
let result = client.recall(&bank_id, &request, false)
|
||||
@@ -361,6 +362,7 @@ impl App {
|
||||
response_schema: None,
|
||||
tags: None,
|
||||
tags_match: TagsMatch::Any,
|
||||
tag_groups: None,
|
||||
};
|
||||
|
||||
let result = client.reflect(&bank_id, &request, false)
|
||||
|
||||
@@ -282,6 +282,7 @@ pub fn recall(
|
||||
include,
|
||||
tags: None,
|
||||
tags_match: TagsMatch::Any,
|
||||
tag_groups: None,
|
||||
};
|
||||
|
||||
let response = client.recall(agent_id, &request, verbose);
|
||||
@@ -340,6 +341,7 @@ pub fn reflect(
|
||||
response_schema,
|
||||
tags: None,
|
||||
tags_match: TagsMatch::Any,
|
||||
tag_groups: None,
|
||||
};
|
||||
|
||||
let response = client.reflect(agent_id, &request, verbose);
|
||||
|
||||
@@ -4314,6 +4314,11 @@ components:
|
||||
- all_strict
|
||||
title: Tags Match
|
||||
type: string
|
||||
tag_groups:
|
||||
items:
|
||||
$ref: '#/components/schemas/RecallRequest_tag_groups_inner'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- query
|
||||
title: RecallRequest
|
||||
@@ -4620,6 +4625,11 @@ components:
|
||||
- all_strict
|
||||
title: Tags Match
|
||||
type: string
|
||||
tag_groups:
|
||||
items:
|
||||
$ref: '#/components/schemas/RecallRequest_tag_groups_inner'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- query
|
||||
title: ReflectRequest
|
||||
@@ -4829,6 +4839,53 @@ components:
|
||||
title: Max Tokens Per Observation
|
||||
type: integer
|
||||
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:
|
||||
description: Single tag with usage count.
|
||||
properties:
|
||||
@@ -5324,6 +5381,19 @@ components:
|
||||
\ which combinations to use."
|
||||
nullable: true
|
||||
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:
|
||||
anyOf:
|
||||
- type: string
|
||||
|
||||
@@ -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"`
|
||||
// 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"`
|
||||
TagGroups []RecallRequestTagGroupsInner `json:"tag_groups,omitempty"`
|
||||
}
|
||||
|
||||
type _RecallRequest RecallRequest
|
||||
@@ -358,6 +359,39 @@ func (o *RecallRequest) SetTagsMatch(v string) {
|
||||
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) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
@@ -393,6 +427,9 @@ func (o RecallRequest) ToMap() (map[string]interface{}, error) {
|
||||
if !IsNil(o.TagsMatch) {
|
||||
toSerialize["tags_match"] = o.TagsMatch
|
||||
}
|
||||
if o.TagGroups != nil {
|
||||
toSerialize["tag_groups"] = o.TagGroups
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
// 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"`
|
||||
TagGroups []RecallRequestTagGroupsInner `json:"tag_groups,omitempty"`
|
||||
}
|
||||
|
||||
type _ReflectRequest ReflectRequest
|
||||
@@ -322,6 +323,39 @@ func (o *ReflectRequest) SetTagsMatch(v string) {
|
||||
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) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
@@ -354,6 +388,9 @@ func (o ReflectRequest) ToMap() (map[string]interface{}, error) {
|
||||
if !IsNil(o.TagsMatch) {
|
||||
toSerialize["tags_match"] = o.TagsMatch
|
||||
}
|
||||
if o.TagGroups != nil {
|
||||
toSerialize["tag_groups"] = o.TagGroups
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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_response.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/operation_response.py
|
||||
hindsight_client_api/models/operation_status_response.py
|
||||
hindsight_client_api/models/operations_list_response.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_result.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/retry_operation_response.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/timestamp.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_response import MentalModelResponse
|
||||
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.operation_response import OperationResponse
|
||||
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.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_result import RecallResult
|
||||
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.retry_operation_response import RetryOperationResponse
|
||||
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.timestamp import Timestamp
|
||||
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_response import MentalModelResponse
|
||||
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.operation_response import OperationResponse
|
||||
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.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_result import RecallResult
|
||||
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.retry_operation_response import RetryOperationResponse
|
||||
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.timestamp import Timestamp
|
||||
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 hindsight_client_api.models.budget import Budget
|
||||
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_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)")
|
||||
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).")
|
||||
__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')
|
||||
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
|
||||
if self.include:
|
||||
_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
|
||||
# and model_fields_set contains the field
|
||||
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:
|
||||
_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
|
||||
|
||||
@classmethod
|
||||
@@ -126,7 +140,8 @@ class RecallRequest(BaseModel):
|
||||
"query_timestamp": obj.get("query_timestamp"),
|
||||
"include": IncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None,
|
||||
"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
|
||||
|
||||
|
||||
+166
@@ -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 typing import Any, ClassVar, Dict, List, Optional
|
||||
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 typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
@@ -36,7 +37,8 @@ class ReflectRequest(BaseModel):
|
||||
response_schema: Optional[Dict[str, Any]] = 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).")
|
||||
__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')
|
||||
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
|
||||
if self.include:
|
||||
_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
|
||||
# and model_fields_set contains the field
|
||||
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:
|
||||
_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
|
||||
|
||||
@classmethod
|
||||
@@ -124,7 +138,8 @@ class ReflectRequest(BaseModel):
|
||||
"include": ReflectIncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None,
|
||||
"response_schema": obj.get("response_schema"),
|
||||
"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
|
||||
|
||||
|
||||
@@ -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,
|
||||
tags: None,
|
||||
tags_match: types::TagsMatch::Any,
|
||||
tag_groups: None,
|
||||
};
|
||||
let recall_response = client
|
||||
.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).
|
||||
*/
|
||||
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).
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
### 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
|
||||
|
||||
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",
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
@@ -7155,6 +7183,34 @@
|
||||
"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).",
|
||||
"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",
|
||||
@@ -7547,6 +7603,121 @@
|
||||
"title": "SourceFactsIncludeOptions",
|
||||
"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": {
|
||||
"properties": {
|
||||
"tag": {
|
||||
|
||||
Reference in New Issue
Block a user