1313import logging
1414import os
1515import uuid
16+ from datetime import datetime
1617from typing import Any , ClassVar , Literal
1718
1819from pydantic import Field , model_validator
@@ -84,6 +85,17 @@ def _get_auth_method(context: Execution) -> str:
8485 return "SYSTEM"
8586
8687
88+ def _coerce_iso_datetime (value : str | None , field_name : str ) -> datetime | None :
89+ """Parse ISO datetime strings (including trailing Z) to datetime for DB bindings."""
90+ if value is None :
91+ return None
92+ normalized = value [:- 1 ] + "+00:00" if value .endswith ("Z" ) else value
93+ try :
94+ return datetime .fromisoformat (normalized )
95+ except ValueError as e :
96+ raise ValueError (f"'{ field_name } ' must be a valid ISO datetime" ) from e
97+
98+
8799# ===========================================================================
88100# Input / Output Models
89101# ===========================================================================
@@ -156,6 +168,10 @@ class KnowledgeInput(BlockInput):
156168 default = None ,
157169 description = "Filter by category UUIDs" ,
158170 )
171+ as_of : str | None = Field (
172+ default = None ,
173+ description = "Point-in-time filter (ISO datetime) against proposition validity window" ,
174+ )
159175 min_confidence : float | None = Field (
160176 default = None ,
161177 description = "Minimum confidence threshold" ,
@@ -222,6 +238,14 @@ class KnowledgeInput(BlockInput):
222238 "Required for document-derived propositions; omit for agent observations."
223239 ),
224240 )
241+ valid_from : str | None = Field (
242+ default = None ,
243+ description = "World-truth start datetime (ISO). NULL means open start" ,
244+ )
245+ valid_to : str | None = Field (
246+ default = None ,
247+ description = "World-truth end datetime (ISO). NULL means open end" ,
248+ )
225249
226250 # --- Recall fields ---
227251 where : dict [str , Any ] | None = Field (
@@ -510,6 +534,7 @@ async def _op_search(
510534 resolved_categories = None
511535 if inputs .categories :
512536 resolved_categories = await self ._resolve_categories (inputs .categories , backend )
537+ as_of = _coerce_iso_datetime (inputs .as_of , "as_of" )
513538
514539 # Compute query embedding
515540 embedding , _ , _ , _ = await compute_embedding (
@@ -523,6 +548,7 @@ async def _op_search(
523548 query_embedding = embedding ,
524549 source = inputs .source ,
525550 categories = resolved_categories ,
551+ as_of = as_of ,
526552 min_confidence = inputs .min_confidence
527553 if inputs .min_confidence is not None
528554 else DEFAULT_MIN_CONFIDENCE ,
@@ -537,6 +563,7 @@ async def _op_search(
537563 query_text = inputs .query ,
538564 source = inputs .source ,
539565 categories = resolved_categories ,
566+ as_of = as_of ,
540567 min_confidence = inputs .min_confidence
541568 if inputs .min_confidence is not None
542569 else DEFAULT_MIN_CONFIDENCE ,
@@ -674,6 +701,8 @@ async def _op_store(
674701 created_by = _get_audit_user_id (context )
675702 auth_method = _get_auth_method (context )
676703 user_string = _get_user_string_id (context )
704+ valid_from = _coerce_iso_datetime (inputs .valid_from , "valid_from" )
705+ valid_to = _coerce_iso_datetime (inputs .valid_to , "valid_to" )
677706
678707 # Build metadata JSON string with auth info
679708 if auth_method and isinstance (auth_method , str ):
@@ -689,13 +718,15 @@ async def _op_store(
689718 (id, item_id, content, embedding, search_vector,
690719 authority, lifecycle_state, confidence,
691720 embedding_model, embedding_dimensions, metadata,
721+ valid_from, valid_to,
692722 created_by, auth_method, source_name, source_type)
693723 VALUES
694724 ($1::uuid, $2::uuid, $3, $4::vector,
695725 to_tsvector('english', $3),
696726 $5, $6, $7,
697727 $8, $9, $10::jsonb,
698- $11::uuid, $12, $13, $14)
728+ $11::timestamptz, $12::timestamptz,
729+ $13::uuid, $14, $15, $16)
699730 """ ,
700731 (
701732 prop_id ,
@@ -708,6 +739,8 @@ async def _op_store(
708739 model_name ,
709740 dimensions ,
710741 metadata_json ,
742+ valid_from ,
743+ valid_to ,
711744 str (created_by ),
712745 auth_method ,
713746 source_name ,
@@ -881,6 +914,15 @@ def next_param(value: Any) -> str:
881914 f"kp.created_at <= { next_param (inputs .created_before )} ::timestamptz"
882915 )
883916
917+ if inputs .as_of :
918+ as_of_param = next_param (_coerce_iso_datetime (inputs .as_of , "as_of" ))
919+ where_clauses .append (
920+ f"(kp.valid_from IS NULL OR kp.valid_from <= { as_of_param } ::timestamptz)"
921+ )
922+ where_clauses .append (
923+ f"(kp.valid_to IS NULL OR kp.valid_to >= { as_of_param } ::timestamptz)"
924+ )
925+
884926 where_sql = " AND " .join (where_clauses ) if where_clauses else "TRUE"
885927 return where_sql , params
886928
@@ -1259,6 +1301,7 @@ async def _search_with_embeddings(
12591301 resolved_categories = None
12601302 if inputs .categories :
12611303 resolved_categories = await self ._resolve_categories (inputs .categories , backend )
1304+ as_of = _coerce_iso_datetime (inputs .as_of , "as_of" )
12621305
12631306 query_embedding , _ , _ , _ = await compute_embedding (
12641307 text = inputs .query ,
@@ -1271,6 +1314,7 @@ async def _search_with_embeddings(
12711314 query_embedding = query_embedding ,
12721315 source = inputs .source ,
12731316 categories = resolved_categories ,
1317+ as_of = as_of ,
12741318 min_confidence = inputs .min_confidence
12751319 if inputs .min_confidence is not None
12761320 else DEFAULT_MIN_CONFIDENCE ,
@@ -1286,6 +1330,7 @@ async def _search_with_embeddings(
12861330 query_text = inputs .query ,
12871331 source = inputs .source ,
12881332 categories = resolved_categories ,
1333+ as_of = as_of ,
12891334 min_confidence = inputs .min_confidence
12901335 if inputs .min_confidence is not None
12911336 else DEFAULT_MIN_CONFIDENCE ,
0 commit comments