66import threading
77from typing import Annotated , Any , Literal
88
9- from pydantic import BaseModel , Field , TypeAdapter , ValidationError , validate_call
9+ from pydantic import BaseModel , ConfigDict , Field , TypeAdapter , ValidationError , validate_call
1010from sentence_transformers import SentenceTransformer
1111
1212from index_common import SBERT_MODEL
@@ -57,6 +57,8 @@ def _get_sentence_transformer(model_name: str, device: str | None) -> SentenceTr
5757
5858
5959class NodeFilter (BaseModel ):
60+ model_config = ConfigDict (extra = "forbid" )
61+
6062 microservice : str | None = None
6163 module : str | None = None
6264 source_layer : str | None = None
@@ -76,6 +78,86 @@ class NodeFilter(BaseModel):
7678 client_method : str | None = None
7779
7880
81+ _NODEFILTER_FIELD_ORDER : tuple [str , ...] = tuple (NodeFilter .model_fields .keys ())
82+
83+ _NODEFILTER_APPLICABLE_FIELDS : dict [Literal ["symbol" , "route" , "client" ], tuple [str , ...]] = {
84+ "symbol" : (
85+ "microservice" ,
86+ "module" ,
87+ "role" ,
88+ "exclude_roles" ,
89+ "annotation" ,
90+ "capability" ,
91+ "fqn_prefix" ,
92+ "symbol_kind" ,
93+ "symbol_kinds" ,
94+ ),
95+ "route" : (
96+ "microservice" ,
97+ "module" ,
98+ "http_method" ,
99+ "path_prefix" ,
100+ "framework" ,
101+ ),
102+ "client" : (
103+ "microservice" ,
104+ "module" ,
105+ "source_layer" ,
106+ "client_kind" ,
107+ "target_service" ,
108+ "target_path_prefix" ,
109+ "client_method" ,
110+ ),
111+ }
112+
113+
114+ def _ordered_nodefilter_fields (field_names : set [str ]) -> list [str ]:
115+ return [name for name in _NODEFILTER_FIELD_ORDER if name in field_names ]
116+
117+
118+ def _populated_nodefilter_fields (nf : NodeFilter ) -> set [str ]:
119+ populated : set [str ] = set ()
120+ for field_name in _NODEFILTER_FIELD_ORDER :
121+ value = getattr (nf , field_name )
122+ if value is None :
123+ continue
124+ if isinstance (value , list ) and not value :
125+ continue
126+ populated .add (field_name )
127+ return populated
128+
129+
130+ def _nodefilter_inapplicable_fields (kind : Literal ["symbol" , "route" , "client" ], nf : NodeFilter ) -> list [str ]:
131+ populated = _populated_nodefilter_fields (nf )
132+ applicable = set (_NODEFILTER_APPLICABLE_FIELDS [kind ])
133+ return _ordered_nodefilter_fields (populated - applicable )
134+
135+
136+ def _nodefilter_applicability_error (kind : Literal ["symbol" , "route" , "client" ], nf : NodeFilter ) -> str | None :
137+ inapplicable = _nodefilter_inapplicable_fields (kind , nf )
138+ if not inapplicable :
139+ return None
140+ applicable = ", " .join (_NODEFILTER_APPLICABLE_FIELDS [kind ])
141+ bad = ", " .join (inapplicable )
142+ return (
143+ f"Invalid filter for kind='{ kind } ': populated field(s) not applicable: [{ bad } ]. "
144+ f"Applicable field(s): [{ applicable } ]"
145+ )
146+
147+
148+ def _filter_validation_error_message (exc : ValidationError ) -> str :
149+ items : list [str ] = []
150+ for err in exc .errors ():
151+ loc = "." .join (str (part ) for part in err .get ("loc" , ()))
152+ msg = str (err .get ("msg" ) or "invalid value" )
153+ if loc :
154+ items .append (f"{ loc } : { msg } " )
155+ else :
156+ items .append (msg )
157+ details = "; " .join (items ) if items else str (exc )
158+ return f"Invalid filter: { details } "
159+
160+
79161def _coerce_filter (
80162 value : NodeFilter | dict [str , Any ] | str | None ,
81163) -> NodeFilter | dict [str , Any ] | None :
@@ -432,11 +514,16 @@ def search_v2(
432514 model = model ,
433515 )
434516 raw_filter = _coerce_filter (filter )
435- nf = (
436- NodeFilter .model_validate (raw_filter )
437- if raw_filter is not None and not isinstance (raw_filter , NodeFilter )
438- else raw_filter
439- )
517+ try :
518+ nf = (
519+ NodeFilter .model_validate (raw_filter )
520+ if raw_filter is not None and not isinstance (raw_filter , NodeFilter )
521+ else raw_filter
522+ )
523+ except ValidationError as exc :
524+ return SearchOutput (success = False , message = _filter_validation_error_message (exc ))
525+ if nf and (err := _nodefilter_applicability_error ("symbol" , nf )):
526+ return SearchOutput (success = False , message = err )
440527 hits : list [SearchHit ] = []
441528 for row in rows :
442529 if path_contains and path_contains not in str (row .get ("filename" ) or "" ):
@@ -463,7 +550,12 @@ def find_v2(
463550 raw_filter = _coerce_filter (filter )
464551 if raw_filter is None :
465552 raw_filter = {}
466- nf = NodeFilter .model_validate (raw_filter ) if not isinstance (raw_filter , NodeFilter ) else raw_filter
553+ try :
554+ nf = NodeFilter .model_validate (raw_filter ) if not isinstance (raw_filter , NodeFilter ) else raw_filter
555+ except ValidationError as exc :
556+ return FindOutput (success = False , message = _filter_validation_error_message (exc ))
557+ if err := _nodefilter_applicability_error (kind , nf ):
558+ return FindOutput (success = False , message = err )
467559 if kind == "symbol" :
468560 where , params = _symbol_where_from_filter (nf )
469561 params ["lim" ] = int (limit ) + int (offset )
@@ -539,12 +631,15 @@ def neighbors_v2(
539631 label_params = [f"l{ i } " for i in range (len (labels ))]
540632 label_predicate = "(" + " OR " .join (f"label(e) = ${ name } " for name in label_params ) + ")"
541633 g = graph or KuzuGraph .get ()
542- raw_filter = _coerce_filter (filter )
543- nf = (
544- NodeFilter .model_validate (raw_filter )
545- if raw_filter is not None and not isinstance (raw_filter , NodeFilter )
546- else raw_filter
547- )
634+ try :
635+ raw_filter = _coerce_filter (filter )
636+ nf = (
637+ NodeFilter .model_validate (raw_filter )
638+ if raw_filter is not None and not isinstance (raw_filter , NodeFilter )
639+ else raw_filter
640+ )
641+ except ValidationError as exc :
642+ return NeighborsOutput (success = False , message = _filter_validation_error_message (exc ))
548643 origins = [ids ] if isinstance (ids , str ) else list (ids )
549644 results : list [Edge ] = []
550645 for origin_id in origins :
@@ -580,6 +675,8 @@ def neighbors_v2(
580675 other_rec = _load_node_record (g , other_id , other_kind )
581676 if other_rec is None :
582677 continue
678+ if nf and (err := _nodefilter_applicability_error (other_kind , nf )):
679+ return NeighborsOutput (success = False , message = err )
583680 if not _node_matches_filter (other_kind , other_rec , nf ):
584681 continue
585682 attrs = {
0 commit comments